diff --git a/.gradle/9.2.1/checksums/checksums.lock b/.gradle/9.2.1/checksums/checksums.lock deleted file mode 100644 index 4875a26..0000000 Binary files a/.gradle/9.2.1/checksums/checksums.lock and /dev/null differ diff --git a/.gradle/9.2.1/checksums/md5-checksums.bin b/.gradle/9.2.1/checksums/md5-checksums.bin deleted file mode 100644 index 6702819..0000000 Binary files a/.gradle/9.2.1/checksums/md5-checksums.bin and /dev/null differ diff --git a/.gradle/9.2.1/checksums/sha1-checksums.bin b/.gradle/9.2.1/checksums/sha1-checksums.bin deleted file mode 100644 index 6e42c74..0000000 Binary files a/.gradle/9.2.1/checksums/sha1-checksums.bin and /dev/null differ diff --git a/.gradle/9.2.1/executionHistory/executionHistory.bin b/.gradle/9.2.1/executionHistory/executionHistory.bin deleted file mode 100644 index d89353d..0000000 Binary files a/.gradle/9.2.1/executionHistory/executionHistory.bin and /dev/null differ diff --git a/.gradle/9.2.1/executionHistory/executionHistory.lock b/.gradle/9.2.1/executionHistory/executionHistory.lock deleted file mode 100644 index f05af3e..0000000 Binary files a/.gradle/9.2.1/executionHistory/executionHistory.lock and /dev/null differ diff --git a/.gradle/9.2.1/fileChanges/last-build.bin b/.gradle/9.2.1/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/.gradle/9.2.1/fileChanges/last-build.bin and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/fileHashes.bin b/.gradle/9.2.1/fileHashes/fileHashes.bin deleted file mode 100644 index 42648bf..0000000 Binary files a/.gradle/9.2.1/fileHashes/fileHashes.bin and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/fileHashes.lock b/.gradle/9.2.1/fileHashes/fileHashes.lock deleted file mode 100644 index 67908cc..0000000 Binary files a/.gradle/9.2.1/fileHashes/fileHashes.lock and /dev/null differ diff --git a/.gradle/9.2.1/fileHashes/resourceHashesCache.bin b/.gradle/9.2.1/fileHashes/resourceHashesCache.bin deleted file mode 100644 index 0c8c023..0000000 Binary files a/.gradle/9.2.1/fileHashes/resourceHashesCache.bin and /dev/null differ diff --git a/.gradle/9.2.1/gc.properties b/.gradle/9.2.1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index e37a69d..0000000 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index e928d81..0000000 --- a/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Fri Jan 16 18:34:39 KST 2026 -gradle.version=9.2.1 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin deleted file mode 100644 index 53d387d..0000000 Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe deleted file mode 100644 index 04be987..0000000 Binary files a/.gradle/file-system.probe and /dev/null differ diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/build.gradle b/build.gradle index 9ce50fa..e814f54 100644 --- a/build.gradle +++ b/build.gradle @@ -43,9 +43,16 @@ dependencies { testImplementation "org.springframework.boot:spring-boot-starter-test" // Springdoc OpenAPI (Swagger) - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0' + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { diff --git a/build/classes/java/main/ssurent/ssurentbe/SsurentbeApplication.class b/build/classes/java/main/ssurent/ssurentbe/SsurentbeApplication.class deleted file mode 100644 index a4b77b2..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/SsurentbeApplication.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/common/base/BaseEntity.class b/build/classes/java/main/ssurent/ssurentbe/common/base/BaseEntity.class deleted file mode 100644 index 6006c3b..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/common/base/BaseEntity.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/common/base/BaseStatus.class b/build/classes/java/main/ssurent/ssurentbe/common/base/BaseStatus.class deleted file mode 100644 index 0f93111..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/common/base/BaseStatus.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists$AssistsBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists$AssistsBuilder.class deleted file mode 100644 index 8158e76..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists$AssistsBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists.class b/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists.class deleted file mode 100644 index 36658c0..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/assists/entity/Assists.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category$CategoryBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category$CategoryBuilder.class deleted file mode 100644 index 8fb406a..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category$CategoryBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category.class deleted file mode 100644 index 8b52a08..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Category.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items$ItemsBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items$ItemsBuilder.class deleted file mode 100644 index 2babe00..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items$ItemsBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items.class deleted file mode 100644 index f118c42..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/Items.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/itemStatusLog.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/itemStatusLog.class deleted file mode 100644 index fee2602..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/entity/itemStatusLog.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/item/enums/Status.class b/build/classes/java/main/ssurent/ssurentbe/domain/item/enums/Status.class deleted file mode 100644 index 852e7e4..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/item/enums/Status.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory$RentalHistoryBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory$RentalHistoryBuilder.class deleted file mode 100644 index e3e76f2..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory$RentalHistoryBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory.class b/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory.class deleted file mode 100644 index 275f342..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/rental/entity/RentalHistory.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/rental/enums/Status.class b/build/classes/java/main/ssurent/ssurentbe/domain/rental/enums/Status.class deleted file mode 100644 index f21988f..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/rental/enums/Status.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty$PanaltyBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty$PanaltyBuilder.class deleted file mode 100644 index 9bd1017..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty$PanaltyBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty.class deleted file mode 100644 index 5299513..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Panalty.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog$UserPanaltyLogBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog$UserPanaltyLogBuilder.class deleted file mode 100644 index e538e7b..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog$UserPanaltyLogBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.class deleted file mode 100644 index 2864ea0..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users$UsersBuilder.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users$UsersBuilder.class deleted file mode 100644 index 108ea65..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users$UsersBuilder.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users.class deleted file mode 100644 index be4ba1d..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/entity/Users.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.class deleted file mode 100644 index 97de368..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Role.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Role.class deleted file mode 100644 index 2441453..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Role.class and /dev/null differ diff --git a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Status.class b/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Status.class deleted file mode 100644 index 80c5a18..0000000 Binary files a/build/classes/java/main/ssurent/ssurentbe/domain/users/enums/Status.class and /dev/null differ diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html deleted file mode 100644 index 517d6a7..0000000 --- a/build/reports/problems/problems-report.html +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin deleted file mode 100644 index 7550106..0000000 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and /dev/null differ diff --git a/src/main/java/ssurent/ssurentbe/SsurentbeApplication.java b/src/main/java/ssurent/ssurentbe/SsurentbeApplication.java index 41b7a65..61c159d 100644 --- a/src/main/java/ssurent/ssurentbe/SsurentbeApplication.java +++ b/src/main/java/ssurent/ssurentbe/SsurentbeApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SsurentbeApplication { diff --git a/src/main/java/ssurent/ssurentbe/common/base/BaseResponse.java b/src/main/java/ssurent/ssurentbe/common/base/BaseResponse.java new file mode 100644 index 0000000..7d5ca14 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/base/BaseResponse.java @@ -0,0 +1,33 @@ +package ssurent.ssurentbe.common.base; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; +import ssurent.ssurentbe.common.status.ErrorStatus; +import ssurent.ssurentbe.common.status.SuccessStatus; + +@Getter +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BaseResponse { + + private final String code; + private final String message; + private final T data; + + public static BaseResponse success(SuccessStatus status, T data) { + return new BaseResponse<>(status.getCode(), status.getMessage(), data); + } + + public static BaseResponse success(SuccessStatus status) { + return new BaseResponse<>(status.getCode(), status.getMessage(), null); + } + + public static BaseResponse error(ErrorStatus status) { + return new BaseResponse<>(status.getCode(), status.getMessage(), null); + } + + public static BaseResponse error(ErrorStatus status, String message) { + return new BaseResponse<>(status.getCode(), message, null); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/config/SecurityConfig.java b/src/main/java/ssurent/ssurentbe/common/config/SecurityConfig.java new file mode 100644 index 0000000..c603236 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/config/SecurityConfig.java @@ -0,0 +1,48 @@ +package ssurent.ssurentbe.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import ssurent.ssurentbe.common.jwt.JwtAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/swagger-ui/**", "/api-docs/**", "/swagger-ui.html").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/config/SwaggerConfig.java b/src/main/java/ssurent/ssurentbe/common/config/SwaggerConfig.java new file mode 100644 index 0000000..0ab94c4 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/config/SwaggerConfig.java @@ -0,0 +1,33 @@ +package ssurent.ssurentbe.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + String securitySchemeName = "bearerAuth"; + + return new OpenAPI() + .info(new Info() + .title("SSURent API") + .description("SSURent 백엔드 API 문서") + .version("v1.0.0")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("JWT Access Token을 입력하세요. (Bearer 접두사 불필요)"))); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/exception/GeneralException.java b/src/main/java/ssurent/ssurentbe/common/exception/GeneralException.java new file mode 100644 index 0000000..f6fd0ec --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/exception/GeneralException.java @@ -0,0 +1,15 @@ +package ssurent.ssurentbe.common.exception; + +import lombok.Getter; +import ssurent.ssurentbe.common.status.ErrorStatus; + +@Getter +public class GeneralException extends RuntimeException { + + private final ErrorStatus status; + + public GeneralException(ErrorStatus status) { + super(status.getMessage()); + this.status = status; + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/exception/GlobalExceptionHandler.java b/src/main/java/ssurent/ssurentbe/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..73a82b9 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,74 @@ +package ssurent.ssurentbe.common.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import ssurent.ssurentbe.common.base.BaseResponse; +import ssurent.ssurentbe.common.base.BaseStatus; +import ssurent.ssurentbe.common.status.ErrorStatus; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleGeneralException(GeneralException e) { + ErrorStatus status = e.getStatus(); + if (status.getHttpStatus().is5xxServerError()) { + log.error("[*] GeneralException :", e); + } else { + log.error("[*] GeneralException : {}", e.getMessage()); + } + return ResponseEntity + .status(status.getHttpStatus()) + .body(BaseResponse.error(status)); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + String errorMessage = "잘못된 요청입니다: " + e.getMessage(); + log.error("[*] IllegalArgumentException :", e); + return ResponseEntity + .status(ErrorStatus.BAD_REQUEST.getHttpStatus()) + .body(BaseResponse.error(ErrorStatus.BAD_REQUEST, errorMessage)); + } + + @ExceptionHandler(NullPointerException.class) + public ResponseEntity> handleNullPointerException(NullPointerException e) { + String errorMessage = "서버에서 예기치 않은 오류가 발생했습니다. 요청을 처리하는 중에 Null 값이 참조되었습니다."; + log.error("[*] NullPointerException :", e); + return ResponseEntity + .status(ErrorStatus.INTERNAL_SERVER_ERROR.getHttpStatus()) + .body(BaseResponse.error(ErrorStatus.INTERNAL_SERVER_ERROR, errorMessage)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + log.error("[*] Internal Server Error :", e); + return ResponseEntity + .status(ErrorStatus.INTERNAL_SERVER_ERROR.getHttpStatus()) + .body(BaseResponse.error(ErrorStatus.INTERNAL_SERVER_ERROR)); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest webRequest + ) { + BaseStatus errorStatus = ErrorStatus.BAD_REQUEST; + String errorMessage = e.getBindingResult().getFieldErrors().isEmpty() + ? errorStatus.getMessage() + : e.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); + + BaseResponse body = BaseResponse.error(ErrorStatus.BAD_REQUEST, errorMessage); + return handleExceptionInternal(e, body, headers, statusCode, webRequest); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/jwt/JwtAuthenticationFilter.java b/src/main/java/ssurent/ssurentbe/common/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..5707842 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,65 @@ +package ssurent.ssurentbe.common.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import ssurent.ssurentbe.common.base.BaseResponse; +import ssurent.ssurentbe.common.exception.GeneralException; +import ssurent.ssurentbe.common.status.ErrorStatus; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private final JwtTokenProvider jwtTokenProvider; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveToken(request); + + if (StringUtils.hasText(token)) { + try { + jwtTokenProvider.validateToken(token); + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (GeneralException e) { + sendErrorResponse(response, e.getStatus()); + return; + } + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(BEARER_PREFIX.length()); + } + return null; + } + + private void sendErrorResponse(HttpServletResponse response, ErrorStatus status) throws IOException { + response.setStatus(status.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getWriter(), BaseResponse.error(status)); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/jwt/JwtTokenProvider.java b/src/main/java/ssurent/ssurentbe/common/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..053e343 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/jwt/JwtTokenProvider.java @@ -0,0 +1,115 @@ +package ssurent.ssurentbe.common.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import ssurent.ssurentbe.common.exception.GeneralException; +import ssurent.ssurentbe.common.status.ErrorStatus; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Slf4j +@Component +public class JwtTokenProvider { + + private final SecretKey secretKey; + private final long accessTokenValidity; + private final long refreshTokenValidity; + private final UserDetailsService userDetailsService; + + public JwtTokenProvider( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-token-validity}") long accessTokenValidity, + @Value("${jwt.refresh-token-validity}") long refreshTokenValidity, + @Lazy UserDetailsService userDetailsService) { + this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); + this.accessTokenValidity = accessTokenValidity; + this.refreshTokenValidity = refreshTokenValidity; + this.userDetailsService = userDetailsService; + } + + public String createAccessToken(String studentNum) { + return createToken(studentNum, accessTokenValidity, "access"); + } + + public String createRefreshToken(String studentNum) { + return createToken(studentNum, refreshTokenValidity, "refresh"); + } + + private String createToken(String studentNum, long validity, String tokenType) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + validity); + + return Jwts.builder() + .subject(studentNum) + .claim("type", tokenType) + .issuedAt(now) + .expiration(expiration) + .signWith(secretKey) + .compact(); + } + + public Authentication getAuthentication(String token) { + String studentNum = getStudentNum(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(studentNum); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getStudentNum(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return true; + } catch (ExpiredJwtException e) { + log.warn("JWT 토큰 만료: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.JWT_EXPIRED); + } catch (SignatureException e) { + log.warn("JWT 서명 검증 실패: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.JWT_INVALID); + } catch (MalformedJwtException e) { + log.warn("JWT 형식 오류: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.JWT_INVALID); + } catch (UnsupportedJwtException e) { + log.warn("지원하지 않는 JWT: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.JWT_INVALID); + } catch (IllegalArgumentException e) { + log.warn("JWT 처리 오류: {}", e.getMessage()); + throw new GeneralException(ErrorStatus.JWT_INVALID); + } + } + + public boolean isRefreshToken(String token) { + try { + String type = Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("type", String.class); + return "refresh".equals(type); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/security/CustomUserDetailsService.java b/src/main/java/ssurent/ssurentbe/common/security/CustomUserDetailsService.java new file mode 100644 index 0000000..e0fab76 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/security/CustomUserDetailsService.java @@ -0,0 +1,37 @@ +package ssurent.ssurentbe.common.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import ssurent.ssurentbe.common.status.ErrorStatus; +import ssurent.ssurentbe.domain.users.entity.Users; +import ssurent.ssurentbe.domain.users.enums.Status; +import ssurent.ssurentbe.domain.users.repository.UserRepository; + +import java.util.Collections; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String studentNum) throws UsernameNotFoundException { + Users user = userRepository.findByStudentNum(studentNum) + .orElseThrow(() -> new UsernameNotFoundException(ErrorStatus.USER_NOT_FOUND.getMessage())); + + if(user.isDeleted()){ + throw new UsernameNotFoundException(ErrorStatus.USER_WITHDRAWN.getMessage()); + } + return new User( + user.getStudentNum(), + user.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) + ); + } +} diff --git a/src/main/java/ssurent/ssurentbe/common/status/ErrorStatus.java b/src/main/java/ssurent/ssurentbe/common/status/ErrorStatus.java new file mode 100644 index 0000000..4fb1656 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/status/ErrorStatus.java @@ -0,0 +1,50 @@ +package ssurent.ssurentbe.common.status; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import ssurent.ssurentbe.common.base.BaseStatus; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseStatus { + + COMM_ERROR_STATUS(HttpStatus.BAD_REQUEST, "COMM_400", "잘못된 요청입니다."), + + /** + * Common + */ + BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMM_400", "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMM_401", "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "COMM_403", "접근 권한이 없습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "COMM_404", "요청한 자원을 찾을 수 없습니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "COMM_405", "허용되지 않은 메소드입니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMM_500", "서버 내부 오류입니다."), + + /** + * Auth + */ + DUPLICATE_STUDENT_NUM(HttpStatus.CONFLICT, "AUTH_409", "이미 가입된 학번입니다."), + INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_401", "학번 또는 비밀번호가 일치하지 않습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_404", "사용자를 찾을 수 없습니다."), + USER_WITHDRAWN(HttpStatus.FORBIDDEN, "AUTH_403", "탈퇴한 사용자입니다."), + INVALID_VERIFICATION_CODE(HttpStatus.UNAUTHORIZED, "AUTH_401", "인증 코드가 일치하지 않습니다."), + VERIFICATION_CODE_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH_401", "인증 코드가 만료되었습니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "AUTH_401", "비밀번호가 올바르지 않습니다."), + + /** + * JWT + */ + JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "JWT_401", "토큰이 존재하지 않습니다."), + JWT_EXPIRED(HttpStatus.UNAUTHORIZED, "JWT_401", "만료된 JWT 토큰입니다."), + JWT_INVALID(HttpStatus.UNAUTHORIZED, "JWT_401", "JWT 토큰이 잘못되었습니다."), + JWT_EXTRACT_ID_FAILED(HttpStatus.UNAUTHORIZED, "JWT_401", "토큰에서 사용자 정보를 추출할 수 없습니다."), + JWT_INVALID_TYPE(HttpStatus.UNAUTHORIZED, "JWT_401", "토큰 타입이 유효하지 않습니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "JWT_401", "DB에 저장된 토큰과 일치하지 않습니다."), + REFRESH_TOKEN_MISMATCH(HttpStatus.UNAUTHORIZED, "JWT_401", "리프레시 토큰 정보가 사용자 정보와 일치하지 않습니다."), + JWT_EXTRACT_ROLE_FAILED(HttpStatus.UNAUTHORIZED, "JWT_401", "토큰에서 사용자 Role을 추출할 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/ssurent/ssurentbe/common/status/SuccessStatus.java b/src/main/java/ssurent/ssurentbe/common/status/SuccessStatus.java new file mode 100644 index 0000000..d215307 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/common/status/SuccessStatus.java @@ -0,0 +1,24 @@ +package ssurent.ssurentbe.common.status; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import ssurent.ssurentbe.common.base.BaseStatus; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseStatus { + COMM_SUCCESS_STATUS(HttpStatus.OK, "COMM_200", "성공적으로 처리되었습니다."), + /** + * Auth + */ + LOGIN_SUCCESS(HttpStatus.OK, "AUTH_200", "로그인 성공"), + LOGOUT_SUCCESS(HttpStatus.OK, "AUTH_200", "로그아웃 성공"), + REISSUE_TOKEN_SUCCESS(HttpStatus.OK, "AUTH_200", "토큰 재발급 성공"), + WITHDRAW_SUCCESS(HttpStatus.OK, "AUTH_200", "회원탈퇴 성공"), + SIGNUP_SUCCESS(HttpStatus.CREATED, "AUTH_201", "회원가입 성공"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/ssurent/ssurentbe/domain/assists/entity/Assists.java b/src/main/java/ssurent/ssurentbe/domain/assists/entity/Assists.java index d8df75d..6db120e 100644 --- a/src/main/java/ssurent/ssurentbe/domain/assists/entity/Assists.java +++ b/src/main/java/ssurent/ssurentbe/domain/assists/entity/Assists.java @@ -15,20 +15,20 @@ public class Assists extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @Column(name = "name") private String name; @Column(name = "is_deleted") - private boolean isDeleted; + private boolean deleted; @Column(name = "deleted_at") private LocalDateTime deletedAt; //도우미 삭제 public void withdraw(){ - this.isDeleted = true; + this.deleted = true; this.deletedAt = LocalDateTime.now(); this.name = null; } diff --git a/src/main/java/ssurent/ssurentbe/domain/item/entity/Category.java b/src/main/java/ssurent/ssurentbe/domain/item/entity/Category.java index 95d2643..ac0180c 100644 --- a/src/main/java/ssurent/ssurentbe/domain/item/entity/Category.java +++ b/src/main/java/ssurent/ssurentbe/domain/item/entity/Category.java @@ -15,7 +15,7 @@ public class Category extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @Column(name = "name") private String name; diff --git a/src/main/java/ssurent/ssurentbe/domain/item/entity/Items.java b/src/main/java/ssurent/ssurentbe/domain/item/entity/Items.java index d503dbf..9db6874 100644 --- a/src/main/java/ssurent/ssurentbe/domain/item/entity/Items.java +++ b/src/main/java/ssurent/ssurentbe/domain/item/entity/Items.java @@ -16,7 +16,7 @@ public class Items extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") @@ -33,7 +33,7 @@ public class Items extends BaseEntity { private Status status; @Column(name = "is_deleted") - private boolean isDeleted; + private boolean deleted; @Column(name = "deleted_at") private LocalDateTime deletedAt; diff --git a/src/main/java/ssurent/ssurentbe/domain/rental/entity/RentalHistory.java b/src/main/java/ssurent/ssurentbe/domain/rental/entity/RentalHistory.java index 833e445..0a7f237 100644 --- a/src/main/java/ssurent/ssurentbe/domain/rental/entity/RentalHistory.java +++ b/src/main/java/ssurent/ssurentbe/domain/rental/entity/RentalHistory.java @@ -19,7 +19,7 @@ public class RentalHistory extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "assist_id") diff --git a/src/main/java/ssurent/ssurentbe/domain/users/controller/AuthController.java b/src/main/java/ssurent/ssurentbe/domain/users/controller/AuthController.java new file mode 100644 index 0000000..3f8a473 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/controller/AuthController.java @@ -0,0 +1,48 @@ +package ssurent.ssurentbe.domain.users.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ssurent.ssurentbe.common.base.BaseResponse; +import ssurent.ssurentbe.common.status.SuccessStatus; +import ssurent.ssurentbe.domain.users.controller.docs.AuthApiDocs; +import ssurent.ssurentbe.domain.users.dto.LoginRequest; +import ssurent.ssurentbe.domain.users.dto.SignupRequest; +import ssurent.ssurentbe.domain.users.dto.TokenResponse; +import ssurent.ssurentbe.domain.users.service.AuthService; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController implements AuthApiDocs { + + private final AuthService authService; + + @Override + @PostMapping("/signup") + public ResponseEntity> signup(@RequestBody SignupRequest request) { + TokenResponse data = authService.signup(request); + SuccessStatus status = SuccessStatus.SIGNUP_SUCCESS; + return ResponseEntity.status(status.getHttpStatus()) + .body(BaseResponse.success(status, data)); + } + + @Override + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginRequest request) { + TokenResponse data = authService.login(request); + SuccessStatus status = SuccessStatus.LOGIN_SUCCESS; + return ResponseEntity.status(status.getHttpStatus()) + .body(BaseResponse.success(status, data)); + } + + @Override + @PostMapping("/refresh") + public ResponseEntity> refresh(@RequestHeader("Authorization") String authorization) { + String refreshToken = authorization.replace("Bearer ", ""); + TokenResponse data = authService.refresh(refreshToken); + SuccessStatus status = SuccessStatus.REISSUE_TOKEN_SUCCESS; + return ResponseEntity.status(status.getHttpStatus()) + .body(BaseResponse.success(status, data)); + } +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/controller/docs/AuthApiDocs.java b/src/main/java/ssurent/ssurentbe/domain/users/controller/docs/AuthApiDocs.java new file mode 100644 index 0000000..ea8d199 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/controller/docs/AuthApiDocs.java @@ -0,0 +1,49 @@ +package ssurent.ssurentbe.domain.users.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import ssurent.ssurentbe.common.base.BaseResponse; +import ssurent.ssurentbe.domain.users.dto.LoginRequest; +import ssurent.ssurentbe.domain.users.dto.SignupRequest; +import ssurent.ssurentbe.domain.users.dto.TokenResponse; +import ssurent.ssurentbe.domain.users.dto.TokenResponseWrapper; + +@Tag(name = "Auth", description = "인증 API") +public interface AuthApiDocs { + + @Operation(summary = "회원가입", description = "학번, 이름, 전화번호, 비밀번호로 회원가입합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "회원가입 성공", + content = @Content(schema = @Schema(implementation = TokenResponseWrapper.class))), + @ApiResponse(responseCode = "409", description = "이미 가입된 학번", + content = @Content(schema = @Schema(implementation = BaseResponse.class))) + }) + ResponseEntity> signup(@RequestBody SignupRequest request); + + @Operation(summary = "로그인", description = "학번과 비밀번호로 로그인합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponseWrapper.class))), + @ApiResponse(responseCode = "401", description = "학번 또는 비밀번호 불일치", + content = @Content(schema = @Schema(implementation = BaseResponse.class))), + @ApiResponse(responseCode = "403", description = "탈퇴한 사용자", + content = @Content(schema = @Schema(implementation = BaseResponse.class))) + }) + ResponseEntity> login(@RequestBody LoginRequest request); + + @Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 새로운 액세스 토큰을 발급받습니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponseWrapper.class))), + @ApiResponse(responseCode = "401", description = "유효하지 않은 토큰", + content = @Content(schema = @Schema(implementation = BaseResponse.class))) + }) + ResponseEntity> refresh(@RequestHeader("Authorization") String authorization); +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/dto/LoginRequest.java b/src/main/java/ssurent/ssurentbe/domain/users/dto/LoginRequest.java new file mode 100644 index 0000000..e56997d --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/dto/LoginRequest.java @@ -0,0 +1,7 @@ +package ssurent.ssurentbe.domain.users.dto; + +public record LoginRequest( + String studentNum, + String password +) { +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/dto/SignupRequest.java b/src/main/java/ssurent/ssurentbe/domain/users/dto/SignupRequest.java new file mode 100644 index 0000000..eef27bd --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/dto/SignupRequest.java @@ -0,0 +1,16 @@ +package ssurent.ssurentbe.domain.users.dto; + +public record SignupRequest( + String studentNum, + String name, + String phoneNum, + String password +) { + @Override + public String toString(){ + return "SignupRequest[studentNum=" + studentNum + + ", name=" + name + + ", phoneNum=" + phoneNum + + ", password=***]"; + } +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponse.java b/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponse.java new file mode 100644 index 0000000..8d33d09 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponse.java @@ -0,0 +1,11 @@ +package ssurent.ssurentbe.domain.users.dto; + +public record TokenResponse( + String accessToken, + String refreshToken, + String tokenType +) { + public static TokenResponse of(String accessToken, String refreshToken) { + return new TokenResponse(accessToken, refreshToken, "Bearer"); + } +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponseWrapper.java b/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponseWrapper.java new file mode 100644 index 0000000..dbbc6dd --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/dto/TokenResponseWrapper.java @@ -0,0 +1,11 @@ +package ssurent.ssurentbe.domain.users.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import ssurent.ssurentbe.common.base.BaseResponse; + +@Schema(description = "토큰 응답") +public class TokenResponseWrapper extends BaseResponse { + public TokenResponseWrapper(String code, String message, TokenResponse data) { + super(code, message, data); + } +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/entity/Panalty.java b/src/main/java/ssurent/ssurentbe/domain/users/entity/Panalty.java deleted file mode 100644 index 4254277..0000000 --- a/src/main/java/ssurent/ssurentbe/domain/users/entity/Panalty.java +++ /dev/null @@ -1,22 +0,0 @@ -package ssurent.ssurentbe.domain.users.entity; - -import jakarta.persistence.*; -import lombok.*; -import ssurent.ssurentbe.common.base.BaseEntity; -import ssurent.ssurentbe.domain.users.enums.PanaltyTypes; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "panalty") -public class Panalty extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; - - @Enumerated(EnumType.STRING) - @Column(name = "panalty_type") - private PanaltyTypes panaltyType; -} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.java b/src/main/java/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.java index 91d0933..f327564 100644 --- a/src/main/java/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.java +++ b/src/main/java/ssurent/ssurentbe/domain/users/entity/UserPanaltyLog.java @@ -16,16 +16,12 @@ public class UserPanaltyLog extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private Users userId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "panalty_id") - private Panalty panaltyId; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "items_id") private Items itemsId; @@ -34,6 +30,7 @@ public class UserPanaltyLog extends BaseEntity { @JoinColumn(name = "history_id") private RentalHistory rentalHistoryId; + @Enumerated(EnumType.STRING) @Column(name = "panalty_type") private PanaltyTypes panaltyType; diff --git a/src/main/java/ssurent/ssurentbe/domain/users/entity/Users.java b/src/main/java/ssurent/ssurentbe/domain/users/entity/Users.java index 9027e77..5334ddb 100644 --- a/src/main/java/ssurent/ssurentbe/domain/users/entity/Users.java +++ b/src/main/java/ssurent/ssurentbe/domain/users/entity/Users.java @@ -17,7 +17,7 @@ public class Users extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private String id; + private Long id; @Column(name = "student_Num", unique = true) private String studentNum; @@ -28,12 +28,15 @@ public class Users extends BaseEntity{ @Column(name = "ph_num") private String phoneNum; + @Column(name = "password", nullable = false) + private String password; + @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) private Status status; @Column(name = "is_deleted") - private boolean isDeleted; + private boolean deleted; @Column(name = "role") private Role role; @@ -43,7 +46,7 @@ public class Users extends BaseEntity{ //회원 삭제처리 public void withdraw(){ - this.isDeleted = true; + this.deleted = true; this.deletedAt = LocalDateTime.now(); this.name = null; this.phoneNum = null; diff --git a/src/main/java/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.java b/src/main/java/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.java index 7aaa6aa..c1d4e7b 100644 --- a/src/main/java/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.java +++ b/src/main/java/ssurent/ssurentbe/domain/users/enums/PanaltyTypes.java @@ -2,6 +2,5 @@ public enum PanaltyTypes { OVERDUE, - UNAUTHORIZED_USE, - BANNED + UNAUTHORIZED_USE } diff --git a/src/main/java/ssurent/ssurentbe/domain/users/repository/UserRepository.java b/src/main/java/ssurent/ssurentbe/domain/users/repository/UserRepository.java new file mode 100644 index 0000000..55e4541 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/repository/UserRepository.java @@ -0,0 +1,13 @@ +package ssurent.ssurentbe.domain.users.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ssurent.ssurentbe.domain.users.entity.Users; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByStudentNum(String studentNum); + boolean existsByStudentNum(String studentNum); +} diff --git a/src/main/java/ssurent/ssurentbe/domain/users/service/AuthService.java b/src/main/java/ssurent/ssurentbe/domain/users/service/AuthService.java new file mode 100644 index 0000000..3dfe655 --- /dev/null +++ b/src/main/java/ssurent/ssurentbe/domain/users/service/AuthService.java @@ -0,0 +1,88 @@ +package ssurent.ssurentbe.domain.users.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ssurent.ssurentbe.common.exception.GeneralException; +import ssurent.ssurentbe.common.jwt.JwtTokenProvider; +import ssurent.ssurentbe.common.status.ErrorStatus; +import ssurent.ssurentbe.domain.users.dto.LoginRequest; +import ssurent.ssurentbe.domain.users.dto.SignupRequest; +import ssurent.ssurentbe.domain.users.dto.TokenResponse; +import ssurent.ssurentbe.domain.users.entity.Users; +import ssurent.ssurentbe.domain.users.enums.Role; +import ssurent.ssurentbe.domain.users.enums.Status; +import ssurent.ssurentbe.domain.users.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public TokenResponse signup(SignupRequest request) { + if (userRepository.existsByStudentNum(request.studentNum())) { + throw new GeneralException(ErrorStatus.DUPLICATE_STUDENT_NUM); + } + + Users user = Users.builder() + .studentNum(request.studentNum()) + .name(request.name()) + .phoneNum(request.phoneNum()) + .password(passwordEncoder.encode(request.password())) + .role(Role.NORMAL) + .status(Status.ACTIVE) + .deleted(false) + .build(); + + userRepository.save(user); + + String accessToken = jwtTokenProvider.createAccessToken(user.getStudentNum()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getStudentNum()); + + return TokenResponse.of(accessToken, refreshToken); + } + + public TokenResponse login(LoginRequest request) { + Users user = userRepository.findByStudentNum(request.studentNum()) + .orElseThrow(() -> new GeneralException(ErrorStatus.INVALID_CREDENTIALS)); + + if (!passwordEncoder.matches(request.password(), user.getPassword())) { + throw new GeneralException(ErrorStatus.INVALID_CREDENTIALS); + } + + if (user.isDeleted()) { + throw new GeneralException(ErrorStatus.USER_WITHDRAWN); + } + + String accessToken = jwtTokenProvider.createAccessToken(user.getStudentNum()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getStudentNum()); + + return TokenResponse.of(accessToken, refreshToken); + } + + public TokenResponse refresh(String refreshToken) { + jwtTokenProvider.validateToken(refreshToken); + if (!jwtTokenProvider.isRefreshToken(refreshToken)) { + throw new GeneralException(ErrorStatus.JWT_INVALID_TYPE); + } + + String studentNum = jwtTokenProvider.getStudentNum(refreshToken); + Users user = userRepository.findByStudentNum(studentNum) + .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + + if (user.isDeleted()) { + throw new GeneralException(ErrorStatus.USER_WITHDRAWN); + } + + String newAccessToken = jwtTokenProvider.createAccessToken(studentNum); + String newRefreshToken = jwtTokenProvider.createRefreshToken(studentNum); + + return TokenResponse.of(newAccessToken, newRefreshToken); + } +} \ No newline at end of file