Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -123,6 +124,16 @@ REDIS_PORT=<YOUR-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
Expand Down Expand Up @@ -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)
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@
<artifactId>jsonschema-module-jakarta-validation</artifactId>
<version>4.31.1</version>
</dependency>

<!-- Swagger docs -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand All @@ -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<Object> generateSampleQuizSet() {
QuizResponse quizResponse = quizGenerationService.generateSampleQuiz();
ApiResponse<Object> apiResponse = ApiResponse.builder()
.message("Sample quiz set successfully generated")
@Operation(summary = "Get sample generated quiz")
public ApiResponse<QuizResponse> getSampleQuiz() {
QuizResponse quizResponse = quizGenerationService.getSampleQuiz();
return ApiResponse.<QuizResponse>builder()
.message("Sample quiz successfully generated")
.data(quizResponse)
.build();
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}

@PostMapping("/quiz-sets/default")
public ResponseEntity<Object> 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<QuizResponse> generateQuiz(@Validated(SingleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) {
QuizResponse quizSetResponse = quizGenerationService.generateQuiz(quizGenerationRequest);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<QuizResponse>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<Object> 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<QuizSetResponse> generateQuizSet(@Validated(MultipleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) {
QuizSetResponse quizSetResponse = quizGenerationService.generateQuizSet(quizGenerationRequest);
ApiResponse<Object> apiResponse = ApiResponse.builder()
.message("Quiz set successfully generated")
return ApiResponse.<QuizSetResponse>builder()
.message("Quizzes successfully generated and added to new quiz set")
.data(quizSetResponse)
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,96 +8,101 @@
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.*;

@Controller
@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<Object> createAttempt(@PathVariable int quizId) {
@Operation(summary = "Create new attempt for target quiz")
public ApiResponse<AttemptResponse> createAttempt(@PathVariable int quizId) {
AttemptResponse attemptResponse = attemptService.createNewAttempt(quizId);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<AttemptResponse>builder()
.message("Attempt created successfully")
.data(attemptResponse)
.build();
return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
}

@GetMapping("/{quizId}/attempts")
@JsonView(AttemptView.Basic.class)
public ResponseEntity<Object> getAllAttemptsForQuiz(@PathVariable int quizId,
@Operation(summary = "Get all attempts by page")
public ApiResponse<PageResponse<AttemptResponse>> getAllAttemptsForQuiz(@PathVariable int quizId,
@RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page,
@RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) {
PageResponse<AttemptResponse> attemptResponseList = attemptService.getAllAttemptsByQuizId(quizId, page, size);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<PageResponse<AttemptResponse>>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<Object> getAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
@Operation(summary = "Get attempt and its recorded answers")
public ApiResponse<AttemptResponse> getAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
AttemptResponse attemptResponseList = attemptService.getAttemptByIdAndQuizId(quizId, attemptId);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<AttemptResponse>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<Object> getAttemptResult(@PathVariable int quizId, @PathVariable int attemptId) {
@Operation(summary = "Get attempt with its recorded answers and correct answers")
public ApiResponse<AttemptResponse> getAttemptResult(@PathVariable int quizId, @PathVariable int attemptId) {
AttemptResponse attemptResponseList = attemptService.getAttemptByIdAndQuizId(quizId, attemptId);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<AttemptResponse>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<Object> finishAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
@Operation(summary = "Finish attempt and calculate result")
public ApiResponse<AttemptResponse> finishAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
AttemptResponse attemptResponseList = attemptService.calculateAttemptResult(quizId, attemptId);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<AttemptResponse>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<Object> updateAttemptDetail(@PathVariable int quizId, @PathVariable int attemptId, @Valid @RequestBody AttemptDetailUpdateRequest request) {
@Operation(summary = "Update attempt detail and/or record answer")
public ApiResponse<AttemptResponse.Detail> updateAttemptDetail(@PathVariable int quizId, @PathVariable int attemptId, @Valid @RequestBody AttemptDetailUpdateRequest request) {
AttemptResponse.Detail attemptDetailResponse = attemptService.updateAttemptDetail(quizId, attemptId, request);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<AttemptResponse.Detail>builder()
.message("Attempt detail updated successfully")
.data(attemptDetailResponse)
.build();
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}

@DeleteMapping("/{quizId}/attempts/{attemptId}")
public ResponseEntity<Object> deleteAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
@Operation(summary = "Delete attempt")
public ApiResponse<Void> deleteAttempt(@PathVariable int quizId, @PathVariable int attemptId) {
attemptService.deleteAttemptByIdAndQuizId(quizId, attemptId);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<Void>builder()
.message("Attempt deleted successfully")
.build();
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +34,8 @@ public class AuthenticationController {
* @return ApiResponse containing UserResponse
*/
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Register new user")
ApiResponse<UserResponse> register(@RequestBody @Valid UserCreationRequest request){
UserResponse response = userService.createUser(request);
return ApiResponse.<UserResponse>builder()
Expand All @@ -46,6 +50,7 @@ ApiResponse<UserResponse> register(@RequestBody @Valid UserCreationRequest reque
* @return ApiResponse containing AuthenticationResponse
*/
@PostMapping("/login")
@Operation(summary = "Login with email and password")
ApiResponse<AuthenticationResponse> login(@RequestBody @Valid LoginRequest request){
AuthenticationResponse response = authenticationService.login(request);
return ApiResponse.<AuthenticationResponse>builder()
Expand All @@ -60,6 +65,7 @@ ApiResponse<AuthenticationResponse> login(@RequestBody @Valid LoginRequest reque
* @return ApiResponse containing new AuthenticationResponse
*/
@PostMapping("/refresh")
@Operation(summary = "Refresh JWT Token")
ApiResponse<AuthenticationResponse> refreshToken(@RequestBody @Valid RefreshTokenRequest request){
AuthenticationResponse response = authenticationService.refreshToken(request);
return ApiResponse.<AuthenticationResponse>builder()
Expand All @@ -75,6 +81,8 @@ ApiResponse<AuthenticationResponse> refreshToken(@RequestBody @Valid RefreshToke
* @return ApiResponse with logout confirmation
*/
@PostMapping("/logout")
@Operation(summary = "Logout and blacklist current token")
@SecurityRequirement(name = "Bearer Authentication")
ApiResponse<Void> logout(Authentication authentication){
authenticationService.logout(authentication);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Object> getAllDocuments(
@Operation(summary = "Get all documents by page")
public ApiResponse<PageResponse<Document>> getAllDocuments(
@RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page,
@RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) {
PageResponse<Document> documentList = documentService.getAllDocuments(page, size);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<PageResponse<Document>>builder()
.message("All document fetched successfully")
.data(documentList)
.build();
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}

@DeleteMapping("/{id}")
public ResponseEntity<Object> deleteDocument(@PathVariable int id) {
@Operation(summary = "Delete document")
public ApiResponse<Void> deleteDocument(@PathVariable int id) {
documentService.deleteDocument(id);
ApiResponse<Object> apiResponse = ApiResponse.builder()
return ApiResponse.<Void>builder()
.message("Document deleted successfully")
.build();
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}
}
Loading
Loading