diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e261f76 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM maven:3.9-eclipse-temurin-17 AS builder +WORKDIR /app + +COPY pom.xml . +RUN mvn dependency:go-offline + +COPY src ./src +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jdk-alpine +WORKDIR /app + +COPY --from=builder /app/target/*.jar app.jar + +# Komenda uruchamiająca +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 6dd90c9..4f180e7 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# code-warehouse-backend \ No newline at end of file +# Code Warehouse Backend + +A simple REST API built with **Spring Boot 3** for managing code and users. It includes JWT authentication, MySQL database support, and Swagger API documentation. The application runs in Docker containers. + +--- + +## 🚀 Tech Stack + +- Java 17 +- Spring Boot +- Spring Security + JWT +- MySQL / H2 +- Swagger (OpenAPI) +- Maven +- Docker + +--- + +## ▶️ Getting Started + +```bash +git clone +cd code-warehouse-backend +docker-compose up --build -d +``` +## ▶️ Access + +- **Application:** http://localhost:8082 +- **Swagger (endpoints):** http://localhost:8082/swagger-ui.html + +## 🛑 Stop Services + +```bash +docker-compose down +``` + +## 👥 Authors + +**Backend Team:** +- Dawid Dykacz + +--- + +Made with ❤️ by the Programming Club Team \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6885c1a..14d81b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,23 @@ services: MYSQL_USER: user MYSQL_PASSWORD: user123 ports: - - "3306:3306" \ No newline at end of file + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + + app: + build: . + container_name: spring-app + restart: always + ports: + - "8082:8082" + depends_on: + mysql: + condition: service_healthy + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/mydb?allowPublicKeyRetrieval=true&useSSL=false + SPRING_DATASOURCE_USERNAME: user + SPRING_DATASOURCE_PASSWORD: user123 + SPRING_JPA_HIBERNATE_DDL_AUTO: update \ No newline at end of file diff --git a/pom.xml b/pom.xml index 07efe5a..b94f7c7 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,11 @@ spring-boot-starter-webmvc-test test + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.2 + io.jsonwebtoken diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java index 1285db6..8bda586 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java @@ -30,6 +30,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSession ht .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers(PATH + "auth/**").permitAll() + .requestMatchers( + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + ).permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java index 4de5ffe..5c4b17b 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java @@ -1,5 +1,8 @@ package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -21,6 +24,13 @@ public AuthController(UsersService usersService) { } @PostMapping("/register") + @Operation( + summary = "Register a new user", + description = "Creates a new user account based on the provided credentials. Returns a JWT token, which might be null under certain conditions (e.g., pending email verification)." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User successfully registered if jwt is not null. Check the response body for the JWT token (can be null)."), + }) public ResponseEntity register(@RequestBody UserRequest userRequest) { final String token = this.usersService.registerUser(userRequest.getUsername(), userRequest.getPassword()); return ResponseEntity.ok(new TokenResponse(token)); diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java index 5995e46..7d7b512 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java @@ -1,5 +1,6 @@ package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,5 +11,10 @@ @NoArgsConstructor @ToString public class TokenResponse { + @Schema( + description = "JWT authentication token.", + nullable = true, + example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ) private String token; } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsException.java b/src/main/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsException.java new file mode 100644 index 0000000..6622e05 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsException.java @@ -0,0 +1,9 @@ +package pl.milosnicyit.codewarehousebackend.exeptions; + +import lombok.NonNull; + +public class UserAlreadyExistsException extends RuntimeException { + public UserAlreadyExistsException(@NonNull final String username) { + super("Username " + username + " already exists"); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java index 719ee6f..eb387bd 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java @@ -1,19 +1,32 @@ package pl.milosnicyit.codewarehousebackend.handlers; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import pl.milosnicyit.codewarehousebackend.exeptions.UserAlreadyExistsException; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception) { return ResponseEntity.badRequest().body(new HandlerDTO(exception.getMessage())); } @ExceptionHandler(UsernameNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException exception) { - return ResponseEntity.badRequest().body(new HandlerDTO(exception.getMessage())); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new HandlerDTO(exception.getMessage())); + } + + @ExceptionHandler(UserAlreadyExistsException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ResponseEntity handleUserAlreadyExistsException(UserAlreadyExistsException exception) { + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(new HandlerDTO(exception.getMessage())); } } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java index a1daaab..6bbfbb8 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java @@ -1,6 +1,7 @@ package pl.milosnicyit.codewarehousebackend.users; import org.commons.login.Password; +import pl.milosnicyit.codewarehousebackend.exeptions.UserAlreadyExistsException; import pl.milosnicyit.codewarehousebackend.jwt.JWTService; import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserDTO; @@ -20,7 +21,7 @@ public UserAppService(UserRepositoryWrapper userRepositoryWrapper, PasswordEncod public String registerUser(String username, String rawPassword) { if (userRepositoryWrapper.existsByUsername(username)) { - return null; + throw new UserAlreadyExistsException(username); } final Password password = new Password(rawPassword); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3b43a49..b05b1f6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,6 +7,6 @@ spring.datasource.password=user123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java index c6fb160..e4a1414 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java @@ -52,8 +52,8 @@ void shouldReturnEmptyTokenWhenUserAlreadyExistsEndToEnd() throws Exception { mockMvc.perform(post(REGISTER_ENDPOINT) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.token").isEmpty()); + .andExpect(status().is4xxClientError()) + .andExpect(jsonPath("$.error").isNotEmpty()); } private String getUserRequestJson(String username, String password) throws JsonProcessingException { diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java index e5da739..da0eb83 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java @@ -7,13 +7,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.exeptions.UserAlreadyExistsException; import pl.milosnicyit.codewarehousebackend.jwt.JWTService; import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserDTO; import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserRepositoryWrapper; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -33,15 +33,15 @@ class UserAppServiceTest { private UserAppService userAppService; @Test - void shouldReturnNullWhenUsernameIsAlreadyTaken() { + void shouldThrowExceptionWhenUsernameIsAlreadyTaken() { String username = "zajetyLogin"; String rawPassword = "secretPassworddddddddddddddddddddddddddddddddddddd"; when(userRepositoryWrapper.existsByUsername(username)).thenReturn(true); - - String result = userAppService.registerUser(username, rawPassword); - - assertNull(result, "Should return null if user already exists"); + assertThrows( + UserAlreadyExistsException.class, + () -> userAppService.registerUser(username, rawPassword) + ); verify(userRepositoryWrapper, never()).save(any()); verify(jwtService, never()).generateToken(anyString());