diff --git a/src/main/java/roomescape/PageController.java b/src/main/java/roomescape/PageController.java index ac8ef9408..5e8aab01a 100644 --- a/src/main/java/roomescape/PageController.java +++ b/src/main/java/roomescape/PageController.java @@ -1,7 +1,14 @@ package roomescape; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import roomescape.member.LoginMember; +import roomescape.member.Member; @Controller public class PageController { diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 000000000..2bdb1f982 --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,31 @@ +package roomescape.config; + +import java.util.List; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.member.LoginMemberArgumentResolver; +import roomescape.member.LoginMemberInterceptor; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final LoginMemberInterceptor loginMemberInterceptor; + + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, LoginMemberInterceptor loginMemberInterceptor) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.loginMemberInterceptor = loginMemberInterceptor; + } + + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loginMemberInterceptor).addPathPatterns("/admin"); + } +} diff --git a/src/main/java/roomescape/member/LoginMember.java b/src/main/java/roomescape/member/LoginMember.java new file mode 100644 index 000000000..36a8bfc07 --- /dev/null +++ b/src/main/java/roomescape/member/LoginMember.java @@ -0,0 +1,8 @@ +package roomescape.member; + +public class LoginMember extends Member { + + public LoginMember(final Long id, final String name, final String email, final String role) { + super(id, name, email, role); + } +} diff --git a/src/main/java/roomescape/member/LoginMemberArgumentResolver.java b/src/main/java/roomescape/member/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..520677d5b --- /dev/null +++ b/src/main/java/roomescape/member/LoginMemberArgumentResolver.java @@ -0,0 +1,42 @@ +package roomescape.member; + +import jakarta.servlet.http.HttpServletRequest; +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 +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberService memberService; + private final TokenUtil tokenUtil; + + public LoginMemberArgumentResolver(MemberService memberService, TokenUtil tokenUtil) { + this.memberService = memberService; + this.tokenUtil = tokenUtil; + } + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.getParameterType().equals(LoginMember.class); + + } + + @Override + public Object resolveArgument( + final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory) + throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + Long memberId = tokenUtil.getMemberId(request.getCookies()); + + Member member = memberService.find(memberId); + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + + } +} diff --git a/src/main/java/roomescape/member/LoginMemberInterceptor.java b/src/main/java/roomescape/member/LoginMemberInterceptor.java new file mode 100644 index 000000000..53f07f93f --- /dev/null +++ b/src/main/java/roomescape/member/LoginMemberInterceptor.java @@ -0,0 +1,31 @@ +package roomescape.member; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class LoginMemberInterceptor implements HandlerInterceptor { + + private final TokenUtil tokenUtil; + private final MemberService memberService; + + public LoginMemberInterceptor(TokenUtil tokenUtil, MemberService memberService) { + this.tokenUtil = tokenUtil; + this.memberService = memberService; + } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Long memberId = this.tokenUtil.getMemberId(request.getCookies()); + + Member member = this.memberService.find(memberId); + + if (member == null || !member.getRole().equals("ADMIN")) { + response.setStatus(401); + return false; + } + + return true; + } +} diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..40a2b693f 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -13,25 +14,56 @@ @RestController public class MemberController { - private MemberService memberService; - - public MemberController(MemberService memberService) { - this.memberService = memberService; - } - - @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { - MemberResponse member = memberService.createMember(memberRequest); - return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - Cookie cookie = new Cookie("token", ""); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); - return ResponseEntity.ok().build(); - } + + private final MemberDao memberDao; + private final TokenUtil tokenUtil; + private MemberService memberService; + + public MemberController(MemberService memberService, MemberDao memberDao, TokenUtil tokenUtil) { + this.memberService = memberService; + this.memberDao = memberDao; + this.tokenUtil = tokenUtil; + } + + @PostMapping("/members") + public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { + MemberResponse member = memberService.createMember(memberRequest); + return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map body) { + // HttpServletRequest 가 Request 객체가 아닌가? 일단 Map으로 대체 + String email = body.get("email"); + String password = body.get("password"); + + // TODO: member가 존재하지 않을때의 처리 + Member member = memberDao.findByEmailAndPassword(email, password); + String token = tokenUtil.generate(member); + + // TODO: 쿠키를 header 에 정상적으로 넣도록 수정 + Cookie cookie = new Cookie("token", token); + cookie.setHttpOnly(true); + cookie.setPath("/"); + + return ResponseEntity.ok().header("Set-Cookie", "token=" + token + ";").build(); + } + + @GetMapping("/login/check") + public ResponseEntity> checkLogin(HttpServletRequest request) { + Long memberId = tokenUtil.getMemberId(request.getCookies()); + Member member = memberDao.findById(memberId); + + return ResponseEntity.ok().body(Map.of("name", member.getName())); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response) { + Cookie cookie = new Cookie("token", ""); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index 81f77f4cd..df94f59f6 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -52,4 +52,16 @@ public Member findByName(String name) { name ); } + + public Member findById(final Long memberId) { + return jdbcTemplate.queryForObject("SELECT id, name, email, role FROM member WHERE id = ?", + (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ), + memberId + ); + } } diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..6e55617af 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -10,6 +10,10 @@ public MemberService(MemberDao memberDao) { this.memberDao = memberDao; } + public Member find(Long memberId) { + return this.memberDao.findById(memberId); + } + public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); diff --git a/src/main/java/roomescape/member/TokenUtil.java b/src/main/java/roomescape/member/TokenUtil.java new file mode 100644 index 000000000..4b3da3536 --- /dev/null +++ b/src/main/java/roomescape/member/TokenUtil.java @@ -0,0 +1,58 @@ +package roomescape.member; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import java.security.Key; +import java.util.Date; +import org.springframework.stereotype.Component; + + +@Component +public class TokenUtil { + + private Key key; + + public TokenUtil() { + this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512); + } + + public String generate(final Member member) { + Date now = new Date(); + + // duration 1시간으로 가정 + int durationSecond = 60 * 60; + Date expirationDate = new Date(now.getTime() + 1000L * durationSecond); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getName()) + .signWith(this.key) + .setIssuedAt(now) + .setExpiration(expirationDate) + .compact(); + } + + public Long getMemberId(final Cookie[] cookies) { + + String token = this.extractTokenFromCookie(cookies); + + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(this.key) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + } + + private String extractTokenFromCookie(Cookie[] cookies) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + + return ""; + } +} diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..6c6277ca5 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -10,37 +10,41 @@ import java.net.URI; import java.util.List; +import roomescape.member.LoginMember; +import roomescape.member.Member; @RestController public class ReservationController { - private final ReservationService reservationService; + private final ReservationService reservationService; - public ReservationController(ReservationService reservationService) { - this.reservationService = reservationService; - } + public ReservationController(ReservationService reservationService) { + this.reservationService = reservationService; + } - @GetMapping("/reservations") - public List list() { - return reservationService.findAll(); - } + @GetMapping("/reservations") + public List list() { + return reservationService.findAll(); + } - @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null - || reservationRequest.getTheme() == null - || reservationRequest.getTime() == null) { - return ResponseEntity.badRequest().build(); - } - ReservationResponse reservation = reservationService.save(reservationRequest); - - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + @PostMapping("/reservations") + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember member) { + if ((member == null && reservationRequest.getName() == null) + || reservationRequest.getDate() == null + || reservationRequest.getTheme() == null + || reservationRequest.getTime() == null) { + return ResponseEntity.badRequest().build(); } - @DeleteMapping("/reservations/{id}") - public ResponseEntity delete(@PathVariable Long id) { - reservationService.deleteById(id); - return ResponseEntity.noContent().build(); - } + ReservationResponse reservation = reservationService.save(member, reservationRequest); + + return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())) + .body(reservation); + } + + @DeleteMapping("/reservations/{id}") + public ResponseEntity delete(@PathVariable Long id) { + reservationService.deleteById(id); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..ee369362a 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -21,4 +21,8 @@ public Long getTheme() { public Long getTime() { return time; } + + public void setName(final String name) { + this.name = name; + } } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..118fa6c3a 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -3,6 +3,8 @@ import org.springframework.stereotype.Service; import java.util.List; +import roomescape.member.LoginMember; +import roomescape.member.Member; @Service public class ReservationService { @@ -12,6 +14,15 @@ public ReservationService(ReservationDao reservationDao) { this.reservationDao = reservationDao; } + public ReservationResponse save(LoginMember member, ReservationRequest reservationRequest) { + if (reservationRequest.getName() == null) { + reservationRequest.setName(member.getName()); + } + Reservation reservation = reservationDao.save(reservationRequest); + + return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + } + public ReservationResponse save(ReservationRequest reservationRequest) { Reservation reservation = reservationDao.save(reservationRequest); diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 6add784bd..f0b41a6e8 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.Map; +import roomescape.reservation.ReservationResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -17,22 +18,89 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { - @Test - void 일단계() { - Map params = new HashMap<>(); - params.put("email", "admin@email.com"); - params.put("password", "password"); + @Test + void 일단계() { + String token = createToken("admin@email.com", "password"); + assertThat(token).isNotBlank(); - ExtractableResponse response = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(params) - .when().post("/login") - .then().log().all() - .statusCode(200) - .extract(); + ExtractableResponse checkResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", token) + .when().get("/login/check") + .then().log().all() + .statusCode(200) + .extract(); - String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); + } - assertThat(token).isNotBlank(); - } + private static String createToken(final String mail, final String password) { + Map params = new HashMap<>(); + params.put("email", mail); + params.put("password", password); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/login") + .then().log().all() + .statusCode(200) + .extract(); + + return response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + } + + @Test + void 이단계() { + String token = createToken("admin@email.com", + "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. + + Map params = new HashMap<>(); + params.put("date", "2024-03-01"); + params.put("time", "1"); + params.put("theme", "1"); + + ExtractableResponse response = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + + params.put("name", "브라운"); + + ExtractableResponse adminResponse = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(adminResponse.statusCode()).isEqualTo(201); + assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); + } + + @Test + void 삼단계() { + String brownToken = createToken("brown@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(401); + + String adminToken = createToken("admin@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } } \ No newline at end of file