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
14 changes: 14 additions & 0 deletions src/main/java/in/koreatech/koin/domain/auth/UserAuth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package in.koreatech.koin.domain.auth;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Target({PARAMETER, FIELD})
@Retention(RUNTIME)
public @interface UserAuth {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package in.koreatech.koin.domain.auth.resolver;

import in.koreatech.koin.domain.auth.JwtProvider;
import in.koreatech.koin.domain.auth.UserAuth;
import in.koreatech.koin.domain.auth.exception.AuthException;
import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
@RequiredArgsConstructor
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

private static final String AUTHORIZATION = "Authorization";

private final JwtProvider jwtProvider;
private final UserRepository userRepository;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserAuth.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (nativeRequest == null) {
throw new AuthException("요청 값이 비어있습니다.");
}

String authorizationHeader = nativeRequest.getHeader(AUTHORIZATION);
if (authorizationHeader == null) {
throw new AuthException("인증 헤더값이 비어있습니다. authorizationHeader: " + nativeRequest);
}
Long userId = jwtProvider.getUserId(authorizationHeader);
return userRepository.findById(userId)
.orElseThrow(() -> UserNotFoundException.withDetail("authorizationHeader: " + authorizationHeader));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package in.koreatech.koin.domain.user.controller;

import java.net.URI;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.domain.auth.UserAuth;
import in.koreatech.koin.domain.user.dto.UserLoginRequest;
import in.koreatech.koin.domain.user.dto.UserLoginResponse;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.domain.user.service.UserService;
import jakarta.validation.Valid;
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
Expand All @@ -25,4 +25,10 @@ public ResponseEntity<UserLoginResponse> login(@RequestBody @Valid UserLoginRequ
return ResponseEntity.created(URI.create("/"))
.body(response);
}

@PostMapping("/user/logout")
public ResponseEntity<Void> logout(@UserAuth User user) {
userService.logout(user);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package in.koreatech.koin.domain.user.repository;

import in.koreatech.koin.domain.user.model.UserToken;
import java.util.Optional;

import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.user.model.UserToken;

public interface UserTokenRepository extends Repository<UserToken, Long> {

UserToken save(UserToken userToken);

Optional<UserToken> findById(Long userId);

void deleteById(Long id);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package in.koreatech.koin.domain.user.service;

import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import java.time.LocalDateTime;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import in.koreatech.koin.domain.auth.JwtProvider;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.domain.user.model.UserToken;
import in.koreatech.koin.domain.user.dto.UserLoginRequest;
import in.koreatech.koin.domain.user.dto.UserLoginResponse;
import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.domain.user.model.UserToken;
import in.koreatech.koin.domain.user.repository.UserRepository;
import in.koreatech.koin.domain.user.repository.UserTokenRepository;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -42,4 +40,9 @@ public UserLoginResponse login(UserLoginRequest request) {

return UserLoginResponse.of(accessToken, savedToken.getRefreshToken(), saved.getUserType().getValue());
}

@Transactional
public void logout(User user) {
userTokenRepository.deleteById(user.getId());
}
}
3 changes: 3 additions & 0 deletions src/main/java/in/koreatech/koin/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package in.koreatech.koin.global.config;

import in.koreatech.koin.domain.auth.resolver.StudentArgumentResolver;
import in.koreatech.koin.domain.auth.resolver.UserArgumentResolver;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -11,10 +12,12 @@
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final UserArgumentResolver userArgumentResolver;
private final StudentArgumentResolver studentArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
resolvers.add(studentArgumentResolver);
}
}
53 changes: 53 additions & 0 deletions src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import io.restassured.http.ContentType;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -73,4 +75,55 @@ void userLoginSuccess() {
}
);
}

@Test
@DisplayName("사용자가 로그인 이후 로그아웃을 수행한다")
void userLogoutSuccess() {
User user = User.builder()
.password("1234")
.nickname("주노")
.name("최준호")
.phoneNumber("010-1234-5678")
.userType(UserType.USER)
.email("test@koreatech.ac.kr")
.isAuthed(true)
.isDeleted(false)
.build();

userRepository.save(user);

ExtractableResponse<Response> response = RestAssured
.given()
.log().all()
.body("""
{
"email": "test@koreatech.ac.kr",
"password": "1234"
}
""")
.contentType(ContentType.JSON)
.when()
.log().all()
.post("/user/login")
.then()
.log().all()
.statusCode(HttpStatus.CREATED.value())
.extract();

RestAssured
.given()
.log().all()
.header("Authorization", "BEARER " + response.jsonPath().getString("token"))
.when()
.log().all()
.post("/user/logout")
.then()
.log().all()
.statusCode(HttpStatus.OK.value())
.extract();

Optional<UserToken> token = tokenRepository.findById(user.getId());

Assertions.assertThat(token).isEmpty();
}
}