From b3341c78ca21edffb19c36c86c87648af84c6978 Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Fri, 16 Apr 2021 14:18:40 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20springboot=20security=20oauth2=20?= =?UTF-8?q?google=20authorization=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20JWT/Cookie=20TDD=20=EC=B6=94=EA=B0=80,=20User=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 20 ++- .../com/ddd/moodof/MoodofApplication.java | 3 + .../auth/exception/BadRequestException.java | 15 ++ ...uth2AuthenticationProcessingException.java | 13 ++ .../exception/ResourceNotFoundException.java | 30 ++++ .../configuration/AppProperties.java | 48 ++++++ .../configuration/CookieUtils.java | 66 ++++++++ .../configuration/SecurityConfig.java | 135 ++++++++++++++++ .../configuration/WebMvcConfig.java | 13 ++ .../infrastructure/security/CurrentUser.java | 12 ++ .../security/CustomUserDetailsService.java | 45 ++++++ .../RestAuthenticationEntryPoint.java | 24 +++ .../security/TokenAuthenticationFilter.java | 57 +++++++ .../security/TokenProvider.java | 75 +++++++++ .../security/UserPrincipal.java | 102 ++++++++++++ .../oauth2/CustomOAuth2UserService.java | 82 ++++++++++ ...eOAuth2AuthorizationRequestRepository.java | 49 ++++++ .../OAuth2AuthenticationFailureHandler.java | 38 +++++ .../OAuth2AuthenticationSuccessHandler.java | 90 +++++++++++ .../oauth2/user/GoogleOAuth2UserInfo.java | 30 ++++ .../security/oauth2/user/OAuth2UserInfo.java | 23 +++ .../oauth2/user/OAuth2UserInfoFactory.java | 25 +++ .../adapter/presentation/UserController.java | 36 +++++ .../domain/model/user/AuthProvider.java | 5 + .../ddd/moodof/domain/model/user/User.java | 25 ++- .../domain/model/user/UserRepository.java | 14 ++ src/main/resources/application.yml | 31 +++- .../ddd/moodof/acceptance/AcceptanceTest.java | 2 +- .../moodof/acceptance/AuthorizationTest.java | 146 ++++++++++++++++++ 29 files changed, 1243 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/AppProperties.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/CookieUtils.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationFailureHandler.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/GoogleOAuth2UserInfo.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfo.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/UserController.java create mode 100644 src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java create mode 100644 src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java create mode 100644 src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java diff --git a/build.gradle b/build.gradle index b990a26..95dbfee 100644 --- a/build.gradle +++ b/build.gradle @@ -23,14 +23,20 @@ repositories { dependencies { //apply querydsl implementation 'com.querydsl:querydsl-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' + //apply p6spy implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8' implementation 'io.springfox:springfox-boot-starter:3.0.0' + // apply spring-security + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + compileOnly 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -38,8 +44,12 @@ dependencies { //apply h2 database runtimeOnly 'com.h2database:h2' - // runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompile('com.h2database:h2') + //apply security test + testImplementation 'org.springframework.security:spring-security-test' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } } test { diff --git a/src/main/java/com/ddd/moodof/MoodofApplication.java b/src/main/java/com/ddd/moodof/MoodofApplication.java index 2ca0bd9..3931dec 100644 --- a/src/main/java/com/ddd/moodof/MoodofApplication.java +++ b/src/main/java/com/ddd/moodof/MoodofApplication.java @@ -1,9 +1,12 @@ package com.ddd.moodof; +import com.ddd.moodof.adapter.infrastructure.configuration.AppProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication +@EnableConfigurationProperties(AppProperties.class) public class MoodofApplication { public static void main(String[] args) { diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java new file mode 100644 index 0000000..e011239 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java @@ -0,0 +1,15 @@ +package com.ddd.moodof.adapter.infrastructure.auth.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } + + public BadRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java new file mode 100644 index 0000000..3112840 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java @@ -0,0 +1,13 @@ +package com.ddd.moodof.adapter.infrastructure.auth.exception; + +import org.springframework.security.core.AuthenticationException; + +public class OAuth2AuthenticationProcessingException extends AuthenticationException { + public OAuth2AuthenticationProcessingException(String msg, Throwable t) { + super(msg, t); + } + + public OAuth2AuthenticationProcessingException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..0cc1969 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java @@ -0,0 +1,30 @@ +package com.ddd.moodof.adapter.infrastructure.auth.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + private String resourceName; + private String fieldName; + private Object fieldValue; + + public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) { + super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); + this.resourceName = resourceName; + this.fieldName = fieldName; + this.fieldValue = fieldValue; + } + + public String getResourceName() { + return resourceName; + } + + public String getFieldName() { + return fieldName; + } + + public Object getFieldValue() { + return fieldValue; + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/AppProperties.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/AppProperties.java new file mode 100644 index 0000000..90335af --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/AppProperties.java @@ -0,0 +1,48 @@ +package com.ddd.moodof.adapter.infrastructure.configuration; + +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@ConfigurationProperties(prefix = "app") +public class AppProperties { + private final Auth auth = new Auth(); + private final OAuth2 oauth2 = new OAuth2(); + + public static class Auth { + private String tokenSecret; + private long tokenExpirationMsec; + + public String getTokenSecret() { + return tokenSecret; + } + + public void setTokenSecret(String tokenSecret) { + this.tokenSecret = tokenSecret; + } + + public long getTokenExpirationMsec() { + return tokenExpirationMsec; + } + + public void setTokenExpirationMsec(long tokenExpirationMsec) { + this.tokenExpirationMsec = tokenExpirationMsec; + } + } + + public static final class OAuth2 { + private List authorizedRedirectUris = new ArrayList<>(); + + public List getAuthorizedRedirectUris() { + return authorizedRedirectUris; + } + + public OAuth2 authorizedRedirectUris(List authorizedRedirectUris) { + this.authorizedRedirectUris = authorizedRedirectUris; + return this; + } + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/CookieUtils.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/CookieUtils.java new file mode 100644 index 0000000..168aeb2 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/CookieUtils.java @@ -0,0 +1,66 @@ +package com.ddd.moodof.adapter.infrastructure.configuration; + +import org.springframework.util.SerializationUtils; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Base64; +import java.util.Optional; + +public class CookieUtils { + + public static Optional getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + return Optional.empty(); + } + + public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + public static Cookie addCookieTest(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + return cookie; + } + + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie: cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(Cookie cookie, Class cls) { + return cls.cast(SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()))); + } + + +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java new file mode 100644 index 0000000..6f0e95f --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java @@ -0,0 +1,135 @@ +package com.ddd.moodof.adapter.infrastructure.configuration; + +import com.ddd.moodof.adapter.infrastructure.security.CustomUserDetailsService; +import com.ddd.moodof.adapter.infrastructure.security.RestAuthenticationEntryPoint; +import com.ddd.moodof.adapter.infrastructure.security.TokenAuthenticationFilter; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.CustomOAuth2UserService; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.OAuth2AuthenticationFailureHandler; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.OAuth2AuthenticationSuccessHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +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.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity( + securedEnabled = true, + jsr250Enabled = true, + prePostEnabled = true +) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private CustomUserDetailsService customUserDetailsService; + + @Autowired + private CustomOAuth2UserService customOAuth2UserService; + + @Autowired + private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; + + @Autowired + private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; + + @Autowired + private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + + @Bean + public TokenAuthenticationFilter tokenAuthenticationFilter() { + return new TokenAuthenticationFilter(); + } + + /* + By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save + the authorization request. But, since our service is stateless, we can't save it in + the session. We'll save the request in a Base64 encoded cookie instead. + */ + @Bean + public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() { + return new HttpCookieOAuth2AuthorizationRequestRepository(); + } + + @Override + public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { + authenticationManagerBuilder + .userDetailsService(customUserDetailsService) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + + @Bean(BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .cors() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .csrf() + .disable() + .formLogin() + .disable() + .httpBasic() + .disable() + .exceptionHandling() + .authenticationEntryPoint(new RestAuthenticationEntryPoint()) + .and() + .authorizeRequests() + .antMatchers("/", + "/error", + "/favicon.ico", + "/**/*.png", + "/**/*.gif", + "/**/*.svg", + "/**/*.jpg", + "/**/*.html", + "/**/*.css", + "/**/*.js") + .permitAll() + .antMatchers("/auth/**", "/oauth2/**") + .permitAll() + + .anyRequest() + .authenticated() + .and() + .oauth2Login() + .authorizationEndpoint() + .baseUri("/oauth2/authorize") + .authorizationRequestRepository(cookieAuthorizationRequestRepository()) + .and() + .redirectionEndpoint() + .baseUri("/oauth2/callback/*") + .and() + .userInfoEndpoint() + .userService(customOAuth2UserService) + .and() + .successHandler(oAuth2AuthenticationSuccessHandler) + .failureHandler(oAuth2AuthenticationFailureHandler); + + // Add our custom Token based authentication filter + http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java index 3716808..d0f7983 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @@ -17,4 +18,16 @@ public class WebMvcConfig implements WebMvcConfigurer { public void addArgumentResolvers(List resolvers) { resolvers.add(loginUserIdArgumentResolver); } + + private final long MAX_AGE_SECS = 3600; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(MAX_AGE_SECS); + } } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java new file mode 100644 index 0000000..e64e1c6 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java @@ -0,0 +1,12 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@AuthenticationPrincipal +public @interface CurrentUser { + +} \ No newline at end of file diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java new file mode 100644 index 0000000..eb0d99c --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java @@ -0,0 +1,45 @@ +package com.ddd.moodof.adapter.infrastructure.security; + + + +import com.ddd.moodof.adapter.infrastructure.auth.exception.ResourceNotFoundException; +import com.ddd.moodof.domain.model.user.UserRepository; +import com.ddd.moodof.domain.model.user.User; +import org.springframework.beans.factory.annotation.Autowired; +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 org.springframework.transaction.annotation.Transactional; + +/** + * Created by rajeevkumarsingh on 02/08/17. + */ + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String email) + throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> + new UsernameNotFoundException("User not found with email : " + email) + ); + + return UserPrincipal.create(user); + } + + @Transactional + public UserDetails loadUserById(Long id) { + User user = userRepository.findById(id).orElseThrow( + () -> new ResourceNotFoundException("User", "id", id) + ); + + return UserPrincipal.create(user); + } +} \ No newline at end of file diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..89ac034 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,24 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + logger.error("Responding with unauthorized error. Message - {}", e.getMessage()); + httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, + e.getLocalizedMessage()); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java new file mode 100644 index 0000000..cdc99bf --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java @@ -0,0 +1,57 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private CustomUserDetailsService customUserDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + String jwt = getJwtFromRequest(request); + + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + Long userId = tokenProvider.getUserIdFromToken(jwt); + + UserDetails userDetails = customUserDetailsService.loadUserById(userId); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception ex) { + logger.error("Could not set user authentication in security context", ex); + } + + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7, bearerToken.length()); + } + return null; + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java new file mode 100644 index 0000000..06c0632 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java @@ -0,0 +1,75 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import com.ddd.moodof.adapter.infrastructure.configuration.AppProperties; +import io.jsonwebtoken.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +public class TokenProvider { + + private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class); + + private AppProperties appProperties; + + private String jwtToken = ""; + + public TokenProvider(AppProperties appProperties) { + this.appProperties = appProperties; + } + + public String getToken(){ + return this.jwtToken; + } + public void setToken(String jwtToken){ + this.jwtToken = jwtToken; + } + + public String createToken(Authentication authentication) { + + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + appProperties.getAuth().getTokenExpirationMsec()); + + String jwtToken = Jwts.builder() + .setSubject(Long.toString(userPrincipal.getId())) + .setIssuedAt(new Date()) + .setExpiration(expiryDate) + .signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret()) + .compact(); + setToken(jwtToken); + return jwtToken; + } + + public Long getUserIdFromToken(String token) { + Claims claims = Jwts.parser() + .setSigningKey(appProperties.getAuth().getTokenSecret()) + .parseClaimsJws(token) + .getBody(); + + return Long.parseLong(claims.getSubject()); + } + + public boolean validateToken(String authToken) { + try { + Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken); + return true; + } catch (SignatureException ex) { + logger.error("Invalid JWT signature"); + } catch (MalformedJwtException ex) { + logger.error("Invalid JWT token"); + } catch (ExpiredJwtException ex) { + logger.error("Expired JWT token"); + } catch (UnsupportedJwtException ex) { + logger.error("Unsupported JWT token"); + } catch (IllegalArgumentException ex) { + logger.error("JWT claims string is empty."); + } + return false; + } + +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java new file mode 100644 index 0000000..fc8c738 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java @@ -0,0 +1,102 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import com.ddd.moodof.domain.model.user.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class UserPrincipal implements OAuth2User, UserDetails{ + private Long id; + private String email; + private String password; + private Collection authorities; + private Map attributes; + + public UserPrincipal(Long id, String email, String password, Collection authorities) { + this.id = id; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserPrincipal create(User user) { + List authorities = Collections. + singletonList(new SimpleGrantedAuthority("ROLE_USER")); + + return new UserPrincipal( + user.getId(), + user.getEmail(), + user.getPassword(), + authorities + ); + } + + public static UserPrincipal create(User user, Map attributes) { + UserPrincipal userPrincipal = UserPrincipal.create(user); + userPrincipal.setAttributes(attributes); + return userPrincipal; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + return String.valueOf(id); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java new file mode 100644 index 0000000..38908d6 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,82 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2; + +import com.ddd.moodof.adapter.infrastructure.auth.exception.OAuth2AuthenticationProcessingException; +import com.ddd.moodof.domain.model.user.AuthProvider; +import com.ddd.moodof.domain.model.user.UserRepository; +import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.user.OAuth2UserInfo; +import com.ddd.moodof.adapter.infrastructure.security.oauth2.user.OAuth2UserInfoFactory; +import com.ddd.moodof.domain.model.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +@Service +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + @Autowired + private UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest); + + try { + return processOAuth2User(oAuth2UserRequest, oAuth2User); + } catch (AuthenticationException ex) { + throw ex; + } catch (Exception ex) { + // Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler + throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause()); + } + } + + private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { + OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes()); + if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) { + throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider"); + } + + Optional userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail()); + User user; + if(userOptional.isPresent()) { + user = userOptional.get(); + if(!user.getProvider().equals(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))) { + throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " + + user.getProvider() + " account. Please use your " + user.getProvider() + + " account to login."); + } + user = updateExistingUser(user, oAuth2UserInfo); + } else { + user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo); + } + + return UserPrincipal.create(user, oAuth2User.getAttributes()); + } + + private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) { + User user = new User(); + + user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId())); + user.setProviderId(oAuth2UserInfo.getId()); + user.setNickname(oAuth2UserInfo.getName()); + user.setEmail(oAuth2UserInfo.getEmail()); + user.setProfileUrl(oAuth2UserInfo.getImageUrl()); + return userRepository.save(user); + } + + private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) { + existingUser.setNickname(oAuth2UserInfo.getName()); + existingUser.setProfileUrl(oAuth2UserInfo.getImageUrl()); + return userRepository.save(existingUser); + } + +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java new file mode 100644 index 0000000..ec693ce --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,49 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2; + +import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; +import com.nimbusds.oauth2.sdk.util.StringUtils; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; + private static final int cookieExpireSeconds = 3600; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class)) + .orElse(null); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + if (authorizationRequest == null) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + return; + } + + CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds); + String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); + if (StringUtils.isNotBlank(redirectUriAfterLogin)) { + CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds); + } + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationFailureHandler.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationFailureHandler.java new file mode 100644 index 0000000..349088a --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationFailureHandler.java @@ -0,0 +1,38 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2; + +import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static com.ddd.moodof.adapter.infrastructure.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +@Component +public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Autowired + HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue) + .orElse(("/")); + + targetUrl = UriComponentsBuilder.fromUriString(targetUrl) + .queryParam("error", exception.getLocalizedMessage()) + .build().toUriString(); + + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000..e5721a2 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,90 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2; + +import com.ddd.moodof.adapter.infrastructure.configuration.AppProperties; +import com.ddd.moodof.adapter.infrastructure.auth.exception.BadRequestException; +import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +import static com.ddd.moodof.adapter.infrastructure.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +@Component +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private TokenProvider tokenProvider; + + private AppProperties appProperties; + + private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + + + @Autowired + OAuth2AuthenticationSuccessHandler(TokenProvider tokenProvider, AppProperties appProperties, + HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository) { + this.tokenProvider = tokenProvider; + this.appProperties = appProperties; + this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + String targetUrl = determineTargetUrl(request, response, authentication); + + if (response.isCommitted()) { + logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); + return; + } + + clearAuthenticationAttributes(request, response); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + Optional redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue); + + if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) { + throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication"); + } + + String targetUrl = redirectUri.orElse(getDefaultTargetUrl()); + + String token = tokenProvider.createToken(authentication); + + return UriComponentsBuilder.fromUriString(targetUrl) + .queryParam("token", token) + .build().toUriString(); + } + + protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) { + super.clearAuthenticationAttributes(request); + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + } + + private boolean isAuthorizedRedirectUri(String uri) { + URI clientRedirectUri = URI.create(uri); + + return appProperties.getOauth2().getAuthorizedRedirectUris() + .stream() + .anyMatch(authorizedRedirectUri -> { + // Only validate host and port. Let the clients use different paths if they want to + URI authorizedURI = URI.create(authorizedRedirectUri); + if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) + && authorizedURI.getPort() == clientRedirectUri.getPort()) { + return true; + } + return false; + }); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/GoogleOAuth2UserInfo.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/GoogleOAuth2UserInfo.java new file mode 100644 index 0000000..befa99b --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/GoogleOAuth2UserInfo.java @@ -0,0 +1,30 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2.user; + +import java.util.Map; + +public class GoogleOAuth2UserInfo extends OAuth2UserInfo { + + public GoogleOAuth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return (String) attributes.get("sub"); + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } + + @Override + public String getImageUrl() { + return (String) attributes.get("picture"); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfo.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfo.java new file mode 100644 index 0000000..5a1bf1c --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfo.java @@ -0,0 +1,23 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2.user; + +import java.util.Map; + +public abstract class OAuth2UserInfo { + protected Map attributes; + + public OAuth2UserInfo(Map attributes) { + this.attributes = attributes; + } + + public Map getAttributes() { + return attributes; + } + + public abstract String getId(); + + public abstract String getName(); + + public abstract String getEmail(); + + public abstract String getImageUrl(); +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java new file mode 100644 index 0000000..0c9345f --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java @@ -0,0 +1,25 @@ +package com.ddd.moodof.adapter.infrastructure.security.oauth2.user; + +import com.ddd.moodof.adapter.infrastructure.auth.exception.OAuth2AuthenticationProcessingException; +import com.ddd.moodof.domain.model.user.AuthProvider; + +import java.util.Map; + +public class OAuth2UserInfoFactory { + + public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { + if(registrationId.equalsIgnoreCase(AuthProvider.google.toString())) { + return new GoogleOAuth2UserInfo(attributes); + } + /* + else if (registrationId.equalsIgnoreCase(AuthProvider.facebook.toString())) { + return new FacebookOAuth2UserInfo(attributes); + } else if (registrationId.equalsIgnoreCase(AuthProvider.github.toString())) { + return new GithubOAuth2UserInfo(attributes); + } + */ + else { + throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); + } + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java b/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java new file mode 100644 index 0000000..e578cfe --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java @@ -0,0 +1,36 @@ +package com.ddd.moodof.adapter.presentation; + +import com.ddd.moodof.adapter.infrastructure.auth.exception.ResourceNotFoundException; +import com.ddd.moodof.domain.model.user.UserRepository; +import com.ddd.moodof.adapter.infrastructure.security.CurrentUser; +import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; +import com.ddd.moodof.domain.model.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class UserController { + private final UserRepository userRepository; + private final TokenProvider tokenProvider; + + @GetMapping("/user/me") + @PreAuthorize("hasRole('USER')") + public User getCurrentUser(@CurrentUser UserPrincipal userPrincipal) { + String token = tokenProvider.getToken(); + System.err.println("token = " + token); + return userRepository.findById(userPrincipal.getId()) + .orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId())); + } + @GetMapping("/user/token") + public String getUserToken(@AuthenticationPrincipal User user, @RequestHeader("Authorization") String authorization) { + System.err.println("authorization = " + authorization); + System.err.println("user = " + user); + return authorization; + } +} diff --git a/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java b/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java new file mode 100644 index 0000000..3d26645 --- /dev/null +++ b/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java @@ -0,0 +1,5 @@ +package com.ddd.moodof.domain.model.user; + +public enum AuthProvider { + google +} diff --git a/src/main/java/com/ddd/moodof/domain/model/user/User.java b/src/main/java/com/ddd/moodof/domain/model/user/User.java index 80e9f92..35eacce 100644 --- a/src/main/java/com/ddd/moodof/domain/model/user/User.java +++ b/src/main/java/com/ddd/moodof/domain/model/user/User.java @@ -1,26 +1,43 @@ package com.ddd.moodof.domain.model.user; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; import java.time.LocalDateTime; @Entity @Getter +@Setter @NoArgsConstructor public class User { @Id @GeneratedValue @Column(name = "user_id") private Long id; + @Email + @Column(nullable = false) private String email; + + @JsonIgnore + private String password; + private String nickname; + private String profileUrl; + private LocalDateTime createdTime; + private LocalDateTime modifiedTime; + + @NotNull + @Enumerated(EnumType.STRING) + private AuthProvider provider; + + private String providerId; } diff --git a/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java b/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java new file mode 100644 index 0000000..5e570a8 --- /dev/null +++ b/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java @@ -0,0 +1,14 @@ +package com.ddd.moodof.domain.model.user; +import com.ddd.moodof.domain.model.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByEmail(String email); + + Boolean existsByEmail(String email); + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7cc80f0..61bd60b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,6 +18,35 @@ spring: enabled: true settings: web-allow-others: true + security: + oauth2: + client: + registration: + google: + clientId: 816491450432-8a14tsegerl77m5nul14pa1q0fo3mb5a.apps.googleusercontent.com + clientSecret: mRHdZmxmoae_ow32LSmZGrZk + redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" + scope: + - email + - profile +app: + auth: + tokenSecret: 926D96C90030DD58429D2751AC1BDBBC + tokenExpirationMsec: 84000000 + # 60000:60초 + oauth2: + # After successfully authenticating with the OAuth2 Provider, + # we'll be generating an auth token for the user and sending the token to the + # redirectUri mentioned by the client in the /oauth2/authorize request. + # We're not using cookies because they won't work well in mobile clients. + authorizedRedirectUris: + - http://localhost:3000/oauth2/redirect + - myandroidapp://oauth2/redirect + - myiosapp://oauth2/redirect + logging.level: org.hibernate.SQL: debug -# org.hibernate.type: trace \ No newline at end of file +# org.hibernate.type: trace + +#apply spring security + diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index 53c550b..6791198 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -53,7 +53,7 @@ protected U postWithLogin(T request, String uri, Class response, Long .content(body) .header(AUTHORIZATION, BEARER + token)) .andExpect(MockMvcResultMatchers.status().isCreated()) - .andExpect(MockMvcResultMatchers.header().string(HttpHeaders.LOCATION, Matchers.matchesRegex(uri + "/\\d*"))) + .andExpect(MockMvcResultMatchers.header().string(HttpHeaders.LOCATION, Matchers.matchesRegex(uri + "/com.ddd.moodof.authorization\\d*"))) .andReturn(); return objectMapper.readValue(result.getResponse().getContentAsString(), response); diff --git a/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java b/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java new file mode 100644 index 0000000..371db10 --- /dev/null +++ b/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java @@ -0,0 +1,146 @@ +package com.ddd.moodof.acceptance; + +import com.ddd.moodof.domain.model.user.AuthProvider; +import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; +import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; +import com.ddd.moodof.domain.model.user.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest +public class AuthorizationTest { + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; + private static final int cookieExpireSeconds = 3600; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private WebApplicationContext webappContext; + + @BeforeEach + public void init() { + mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) + .apply(springSecurity()) + .build(); + } + @Test + @WithMockUser(username = "moodof ddd",password = "", authorities = "USER") + public void JWT_토큰_생성() throws Exception { + OAuth2User oAuth2User = Mockito.mock(OAuth2User.class); + Authentication authorizationRequest = Mockito.mock(Authentication.class); + UserPrincipal userPrincipal = mock(UserPrincipal.class); + User user = new User(); + user.setEmail("ddd.moodof@gmail.com"); + user.setNickname("moodof ddd"); + user.setProfileUrl("https://lh6.googleusercontent.com/-4vjsYgdYzVE/AAAAAAAAAAI/AAAAAAAAAAA/AMZuuckpFmWzV5cRiDCebObd-AidXzUC8g/s96-c/photo.jpg"); + user.setProvider(AuthProvider.google); + user.setProviderId("107406868053916247309"); + + userPrincipal.create(user, oAuth2User.getAttributes()); + SecurityContext securityContext = mock(SecurityContext.class); + SecurityContextHolder.setContext(securityContext); + + // Mockito.whens() for your authorization object + when(securityContext.getAuthentication()).thenReturn(authorizationRequest); + when(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).thenReturn(userPrincipal); + when(securityContext.getAuthentication()).thenReturn(authorizationRequest); + System.err.println("authorizationRequest.getPrincipal() = " + authorizationRequest.getPrincipal()); + + /* + JWT 토큰 생성후 값 받아오기 + */ + tokenProvider.createToken(authorizationRequest); + String token = tokenProvider.getToken(); + + /* + JWT 토큰처리 + */ + System.out.println("token = " + token); + mockMvc.perform(RequestSecurityFactory.securityFactory("/user/token", token) + .header(AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk());; + + } + @Test + @WithMockUser(username = "moodof ddd",password = "", authorities = "USER") + public void 토큰_쿠키_생성() throws Exception { + Authentication authorizationCookieRequest = Mockito.mock(Authentication.class); + SecurityContext securityContext = mock(SecurityContext.class); + SecurityContextHolder.setContext(securityContext); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + if (authorizationCookieRequest == null) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + } + Cookie cookie = CookieUtils.addCookieTest(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationCookieRequest), cookieExpireSeconds); + request.setAttribute(REDIRECT_URI_PARAM_COOKIE_NAME, cookie.getValue()); + String token = cookie.getValue(); + System.out.println("token = " + token); + mockMvc.perform(RequestSecurityFactory.securityFactory("/user/token", token) + .header(AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()); + } + + /** + * 인증 토큰값으로 인증가능 + * @throws Exception + */ + @Test + void 토큰_인증_헤더() throws Exception { + String token = ""; + System.err.println(token); + mockMvc.perform(MockMvcRequestBuilders.get("/user/token") + .header(AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()); + } + + /** + * 시큐리티 클라이언트 헤더값 세팅 + */ + public static class RequestSecurityFactory { + /** + * 요청값 헤더값 세팅 + * @param url + * @param token + * @return + */ + public static MockHttpServletRequestBuilder securityFactory(String url, String token) { + return MockMvcRequestBuilders.get(url) + .header(AUTHORIZATION, "Bearer " + token); + } + } + +} \ No newline at end of file From 6516d03f78248be5375e3a793f5d948ef2ef1f7f Mon Sep 17 00:00:00 2001 From: Gyeongjun Kim Date: Sat, 17 Apr 2021 01:09:39 +0900 Subject: [PATCH 02/12] =?UTF-8?q?refactor:=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + build.gradle | 5 +- .../com/ddd/moodof/MoodofApplication.java | 3 +- .../advice/ExceptionHandlingController.java | 2 +- .../advice/ExceptionResponse.java | 2 +- .../exception/ResourceNotFoundException.java | 30 ---- .../configuration/SecurityConfig.java | 62 +++----- .../configuration/WebMvcConfig.java | 6 +- .../infrastructure/security/CurrentUser.java | 12 -- .../security/CustomUserDetailsService.java | 30 +--- .../LoginUserIdArgumentResolver.java | 17 +- .../RestAuthenticationEntryPoint.java | 9 +- .../security/TokenAuthenticationFilter.java | 19 ++- .../security/TokenProvider.java | 44 ++---- .../security/UserPrincipal.java | 8 +- .../exception/BadRequestException.java | 2 +- .../exception/InvalidTokenException.java | 2 +- ...uth2AuthenticationProcessingException.java | 2 +- .../oauth2/CustomOAuth2UserService.java | 31 ++-- .../OAuth2AuthenticationSuccessHandler.java | 42 ++--- .../oauth2/user/OAuth2UserInfoFactory.java | 15 +- .../presentation/StoragePhotoController.java | 8 +- .../adapter/presentation/UserController.java | 37 ++--- .../presentation/api/StoragePhotoAPI.java | 17 ++ .../adapter/presentation/api/UserAPI.java | 14 ++ .../application/StoragePhotoService.java | 4 +- .../ddd/moodof/application/UserService.java | 17 ++ .../application/dto/StoragePhotoDTO.java | 8 +- .../ddd/moodof/application/dto/UserDTO.java | 28 ++++ .../model/storage/photo/StoragePhoto.java | 5 + .../domain/model/user/AuthProvider.java | 2 +- .../ddd/moodof/domain/model/user/User.java | 34 ++-- .../domain/model/user/UserRepository.java | 6 +- src/main/resources/application.yml | 31 +--- .../ddd/moodof/acceptance/AcceptanceTest.java | 38 ++++- .../moodof/acceptance/AuthorizationTest.java | 146 ------------------ .../StoragePhotoAcceptanceTest.java | 9 +- .../moodof/acceptance/UserAcceptanceTest.java | 30 ++++ 38 files changed, 318 insertions(+), 461 deletions(-) rename src/main/java/com/ddd/moodof/adapter/{presentation => infrastructure}/advice/ExceptionHandlingController.java (95%) rename src/main/java/com/ddd/moodof/adapter/{presentation => infrastructure}/advice/ExceptionResponse.java (81%) delete mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java delete mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java rename src/main/java/com/ddd/moodof/adapter/infrastructure/{auth => security}/LoginUserIdArgumentResolver.java (73%) rename src/main/java/com/ddd/moodof/adapter/infrastructure/{auth => security}/exception/BadRequestException.java (85%) rename src/main/java/com/ddd/moodof/adapter/infrastructure/{auth => security}/exception/InvalidTokenException.java (68%) rename src/main/java/com/ddd/moodof/adapter/infrastructure/{auth => security}/exception/OAuth2AuthenticationProcessingException.java (84%) create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/api/UserAPI.java create mode 100644 src/main/java/com/ddd/moodof/application/UserService.java create mode 100644 src/main/java/com/ddd/moodof/application/dto/UserDTO.java delete mode 100644 src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java create mode 100644 src/test/java/com/ddd/moodof/acceptance/UserAcceptanceTest.java diff --git a/.gitignore b/.gitignore index c2065bc..4bf68a5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +application-security.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 95dbfee..2710250 100644 --- a/build.gradle +++ b/build.gradle @@ -42,9 +42,12 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + //apply h2 database runtimeOnly 'com.h2database:h2' - testCompile('com.h2database:h2') + testImplementation 'com.h2database:h2' + //apply security test testImplementation 'org.springframework.security:spring-security-test' testImplementation('org.springframework.boot:spring-boot-starter-test') { diff --git a/src/main/java/com/ddd/moodof/MoodofApplication.java b/src/main/java/com/ddd/moodof/MoodofApplication.java index 3931dec..34a4733 100644 --- a/src/main/java/com/ddd/moodof/MoodofApplication.java +++ b/src/main/java/com/ddd/moodof/MoodofApplication.java @@ -4,10 +4,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication @EnableConfigurationProperties(AppProperties.class) -public class MoodofApplication { +public class MoodofApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(MoodofApplication.class, args); diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionHandlingController.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionHandlingController.java similarity index 95% rename from src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionHandlingController.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionHandlingController.java index 4a5ff2d..44c22f8 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionHandlingController.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionHandlingController.java @@ -1,4 +1,4 @@ -package com.ddd.moodof.adapter.presentation.advice; +package com.ddd.moodof.adapter.infrastructure.advice; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionResponse.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionResponse.java similarity index 81% rename from src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionResponse.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionResponse.java index 62f369c..6bae7be 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/advice/ExceptionResponse.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/advice/ExceptionResponse.java @@ -1,4 +1,4 @@ -package com.ddd.moodof.adapter.presentation.advice; +package com.ddd.moodof.adapter.infrastructure.advice; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java deleted file mode 100644 index 0cc1969..0000000 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/ResourceNotFoundException.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ddd.moodof.adapter.infrastructure.auth.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) -public class ResourceNotFoundException extends RuntimeException { - private String resourceName; - private String fieldName; - private Object fieldValue; - - public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) { - super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); - this.resourceName = resourceName; - this.fieldName = fieldName; - this.fieldValue = fieldValue; - } - - public String getResourceName() { - return resourceName; - } - - public String getFieldName() { - return fieldName; - } - - public Object getFieldValue() { - return fieldValue; - } -} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java index 6f0e95f..1c47d36 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java @@ -7,7 +7,7 @@ import com.ddd.moodof.adapter.infrastructure.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; import com.ddd.moodof.adapter.infrastructure.security.oauth2.OAuth2AuthenticationFailureHandler; import com.ddd.moodof.adapter.infrastructure.security.oauth2.OAuth2AuthenticationSuccessHandler; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -22,6 +22,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@RequiredArgsConstructor @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity( @@ -31,31 +32,12 @@ ) public class SecurityConfig extends WebSecurityConfigurerAdapter { - @Autowired - private CustomUserDetailsService customUserDetailsService; - - @Autowired - private CustomOAuth2UserService customOAuth2UserService; - - @Autowired - private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; - - @Autowired - private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; - - @Autowired - private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; + private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; + private final TokenAuthenticationFilter tokenAuthenticationFilter; - @Bean - public TokenAuthenticationFilter tokenAuthenticationFilter() { - return new TokenAuthenticationFilter(); - } - - /* - By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save - the authorization request. But, since our service is stateless, we can't save it in - the session. We'll save the request in a Base64 encoded cookie instead. - */ @Bean public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() { return new HttpCookieOAuth2AuthorizationRequestRepository(); @@ -82,6 +64,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { + // @formatter:off http .cors() .and() @@ -98,20 +81,17 @@ protected void configure(HttpSecurity http) throws Exception { .authenticationEntryPoint(new RestAuthenticationEntryPoint()) .and() .authorizeRequests() - .antMatchers("/", - "/error", - "/favicon.ico", - "/**/*.png", - "/**/*.gif", - "/**/*.svg", - "/**/*.jpg", - "/**/*.html", - "/**/*.css", - "/**/*.js") - .permitAll() - .antMatchers("/auth/**", "/oauth2/**") - .permitAll() - + .antMatchers( + "/error", + "/auth/**", + "/oauth2/**", + "/h2-console/**", + "/swagger-ui/**", + "/webjars/**", + "/v2/**", + "/swagger-resources/**", + "/api/public/**" + ).permitAll() .anyRequest() .authenticated() .and() @@ -128,8 +108,8 @@ protected void configure(HttpSecurity http) throws Exception { .and() .successHandler(oAuth2AuthenticationSuccessHandler) .failureHandler(oAuth2AuthenticationFailureHandler); + // @formatter:on - // Add our custom Token based authentication filter - http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java index d0f7983..69ef7ac 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java @@ -1,6 +1,6 @@ package com.ddd.moodof.adapter.infrastructure.configuration; -import com.ddd.moodof.adapter.infrastructure.auth.LoginUserIdArgumentResolver; +import com.ddd.moodof.adapter.infrastructure.security.LoginUserIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -12,6 +12,8 @@ @RequiredArgsConstructor @Configuration public class WebMvcConfig implements WebMvcConfigurer { + private static final long MAX_AGE_SECS = 3600L; + private final LoginUserIdArgumentResolver loginUserIdArgumentResolver; @Override @@ -19,8 +21,6 @@ public void addArgumentResolvers(List resolvers) resolvers.add(loginUserIdArgumentResolver); } - private final long MAX_AGE_SECS = 3600; - @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java deleted file mode 100644 index e64e1c6..0000000 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CurrentUser.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.ddd.moodof.adapter.infrastructure.security; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import java.lang.annotation.*; - -@Target({ElementType.PARAMETER, ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@AuthenticationPrincipal -public @interface CurrentUser { - -} \ No newline at end of file diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java index eb0d99c..8dc9088 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/CustomUserDetailsService.java @@ -1,44 +1,30 @@ package com.ddd.moodof.adapter.infrastructure.security; - -import com.ddd.moodof.adapter.infrastructure.auth.exception.ResourceNotFoundException; -import com.ddd.moodof.domain.model.user.UserRepository; import com.ddd.moodof.domain.model.user.User; -import org.springframework.beans.factory.annotation.Autowired; +import com.ddd.moodof.domain.model.user.UserRepository; +import lombok.RequiredArgsConstructor; 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 org.springframework.transaction.annotation.Transactional; - -/** - * Created by rajeevkumarsingh on 02/08/17. - */ +@RequiredArgsConstructor @Service public class CustomUserDetailsService implements UserDetailsService { - - @Autowired - UserRepository userRepository; + private final UserRepository userRepository; @Override - @Transactional - public UserDetails loadUserByUsername(String email) - throws UsernameNotFoundException { + public UserDetails loadUserByUsername(String email) { User user = userRepository.findByEmail(email) - .orElseThrow(() -> - new UsernameNotFoundException("User not found with email : " + email) - ); + .orElseThrow(() -> new UsernameNotFoundException("User not found with email : " + email)); return UserPrincipal.create(user); } - @Transactional public UserDetails loadUserById(Long id) { - User user = userRepository.findById(id).orElseThrow( - () -> new ResourceNotFoundException("User", "id", id) - ); + User user = userRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("no user, id = " + id)); return UserPrincipal.create(user); } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/LoginUserIdArgumentResolver.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/LoginUserIdArgumentResolver.java similarity index 73% rename from src/main/java/com/ddd/moodof/adapter/infrastructure/auth/LoginUserIdArgumentResolver.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/security/LoginUserIdArgumentResolver.java index 0635baa..c48e92e 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/LoginUserIdArgumentResolver.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/LoginUserIdArgumentResolver.java @@ -1,7 +1,9 @@ -package com.ddd.moodof.adapter.infrastructure.auth; +package com.ddd.moodof.adapter.infrastructure.security; -import com.ddd.moodof.adapter.infrastructure.auth.exception.InvalidTokenException; +import com.ddd.moodof.adapter.infrastructure.security.exception.InvalidTokenException; import com.ddd.moodof.adapter.presentation.LoginUserId; +import com.ddd.moodof.domain.model.user.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -13,6 +15,7 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; +@RequiredArgsConstructor @Component public class LoginUserIdArgumentResolver implements HandlerMethodArgumentResolver { private static final String SP = " "; @@ -20,6 +23,9 @@ public class LoginUserIdArgumentResolver implements HandlerMethodArgumentResolve private static final int TYPE_INDEX = 0; private static final String BEARER = "bearer"; + private final UserRepository userRepository; + private final TokenProvider tokenProvider; + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(LoginUserId.class); @@ -29,16 +35,17 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { String[] authorizations = Objects.requireNonNull(webRequest.getHeader(AUTHORIZATION)).split(SP); String type = authorizations[TYPE_INDEX]; - // TODO: 2021/04/07 현재는 userId String token = authorizations[TOKEN_INDEX]; if (!type.equalsIgnoreCase(BEARER)) { throw new InvalidTokenException("지원하지 않는 토큰 타입입니다. type : " + type); } - Long userId = Long.parseLong(token); + Long userId = tokenProvider.getUserId(token); - // TODO: 2021/04/07 User 구현시, UserRepository.existById(userId) + if (!userRepository.existsById(userId)) { + throw new IllegalArgumentException("존재하지 않는 user, userId = " + userId); + } return userId; } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java index 89ac034..ce6e028 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/RestAuthenticationEntryPoint.java @@ -1,23 +1,22 @@ package com.ddd.moodof.adapter.infrastructure.security; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +@Slf4j public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { - private static final Logger logger = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); - @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { - logger.error("Responding with unauthorized error. Message - {}", e.getMessage()); + log.error("Responding with unauthorized error. Message - {}", e.getMessage()); httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getLocalizedMessage()); } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java index cdc99bf..f642e95 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenAuthenticationFilter.java @@ -1,12 +1,13 @@ package com.ddd.moodof.adapter.infrastructure.security; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -16,23 +17,21 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +@RequiredArgsConstructor +@Component public class TokenAuthenticationFilter extends OncePerRequestFilter { - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private CustomUserDetailsService customUserDetailsService; - private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class); + private final TokenProvider tokenProvider; + private final CustomUserDetailsService customUserDetailsService; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { - Long userId = tokenProvider.getUserIdFromToken(jwt); + Long userId = tokenProvider.getUserId(jwt); UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); @@ -50,7 +49,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7, bearerToken.length()); + return bearerToken.substring(7); } return null; } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java index 06c0632..d5f145d 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/TokenProvider.java @@ -2,50 +2,34 @@ import com.ddd.moodof.adapter.infrastructure.configuration.AppProperties; import io.jsonwebtoken.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; import java.util.Date; -@Service +@Slf4j +@Component public class TokenProvider { - - private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class); - - private AppProperties appProperties; - - private String jwtToken = ""; + private final AppProperties appProperties; public TokenProvider(AppProperties appProperties) { this.appProperties = appProperties; } - public String getToken(){ - return this.jwtToken; - } - public void setToken(String jwtToken){ - this.jwtToken = jwtToken; - } - - public String createToken(Authentication authentication) { + public String createToken(Long userId) { - UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + appProperties.getAuth().getTokenExpirationMsec()); - String jwtToken = Jwts.builder() - .setSubject(Long.toString(userPrincipal.getId())) + return Jwts.builder() + .setSubject(Long.toString(userId)) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret()) .compact(); - setToken(jwtToken); - return jwtToken; } - public Long getUserIdFromToken(String token) { + public Long getUserId(String token) { Claims claims = Jwts.parser() .setSigningKey(appProperties.getAuth().getTokenSecret()) .parseClaimsJws(token) @@ -59,15 +43,15 @@ public boolean validateToken(String authToken) { Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken); return true; } catch (SignatureException ex) { - logger.error("Invalid JWT signature"); + log.error("Invalid JWT signature"); } catch (MalformedJwtException ex) { - logger.error("Invalid JWT token"); + log.error("Invalid JWT token"); } catch (ExpiredJwtException ex) { - logger.error("Expired JWT token"); + log.error("Expired JWT token"); } catch (UnsupportedJwtException ex) { - logger.error("Unsupported JWT token"); + log.error("Unsupported JWT token"); } catch (IllegalArgumentException ex) { - logger.error("JWT claims string is empty."); + log.error("JWT claims string is empty."); } return false; } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java index fc8c738..93939f9 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/UserPrincipal.java @@ -11,10 +11,10 @@ import java.util.List; import java.util.Map; -public class UserPrincipal implements OAuth2User, UserDetails{ - private Long id; - private String email; - private String password; +public class UserPrincipal implements OAuth2User, UserDetails { + private final Long id; + private final String email; + private final String password; private Collection authorities; private Map attributes; diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/BadRequestException.java similarity index 85% rename from src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/BadRequestException.java index e011239..50af9ff 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/BadRequestException.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/BadRequestException.java @@ -1,4 +1,4 @@ -package com.ddd.moodof.adapter.infrastructure.auth.exception; +package com.ddd.moodof.adapter.infrastructure.security.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/InvalidTokenException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/InvalidTokenException.java similarity index 68% rename from src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/InvalidTokenException.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/InvalidTokenException.java index 77296f0..10c5182 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/InvalidTokenException.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/InvalidTokenException.java @@ -1,4 +1,4 @@ -package com.ddd.moodof.adapter.infrastructure.auth.exception; +package com.ddd.moodof.adapter.infrastructure.security.exception; public class InvalidTokenException extends RuntimeException { public InvalidTokenException(String message) { diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/OAuth2AuthenticationProcessingException.java similarity index 84% rename from src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java rename to src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/OAuth2AuthenticationProcessingException.java index 3112840..7d9fc2d 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/auth/exception/OAuth2AuthenticationProcessingException.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/exception/OAuth2AuthenticationProcessingException.java @@ -1,4 +1,4 @@ -package com.ddd.moodof.adapter.infrastructure.auth.exception; +package com.ddd.moodof.adapter.infrastructure.security.exception; import org.springframework.security.core.AuthenticationException; diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java index 38908d6..003e196 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/CustomOAuth2UserService.java @@ -1,13 +1,13 @@ package com.ddd.moodof.adapter.infrastructure.security.oauth2; -import com.ddd.moodof.adapter.infrastructure.auth.exception.OAuth2AuthenticationProcessingException; -import com.ddd.moodof.domain.model.user.AuthProvider; -import com.ddd.moodof.domain.model.user.UserRepository; import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; +import com.ddd.moodof.adapter.infrastructure.security.exception.OAuth2AuthenticationProcessingException; import com.ddd.moodof.adapter.infrastructure.security.oauth2.user.OAuth2UserInfo; import com.ddd.moodof.adapter.infrastructure.security.oauth2.user.OAuth2UserInfoFactory; +import com.ddd.moodof.domain.model.user.AuthProvider; import com.ddd.moodof.domain.model.user.User; -import org.springframework.beans.factory.annotation.Autowired; +import com.ddd.moodof.domain.model.user.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -15,15 +15,14 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import java.util.Optional; +@RequiredArgsConstructor @Service public class CustomOAuth2UserService extends DefaultOAuth2UserService { - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; @Override public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { @@ -41,15 +40,15 @@ public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2Aut private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes()); - if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) { + if (oAuth2UserInfo.getEmail().isEmpty()) { throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider"); } Optional userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail()); User user; - if(userOptional.isPresent()) { + if (userOptional.isPresent()) { user = userOptional.get(); - if(!user.getProvider().equals(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))) { + if (!user.getProvider().equals(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))) { throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " + user.getProvider() + " account. Please use your " + user.getProvider() + " account to login."); @@ -63,19 +62,13 @@ private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2 } private User registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) { - User user = new User(); - - user.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId())); - user.setProviderId(oAuth2UserInfo.getId()); - user.setNickname(oAuth2UserInfo.getName()); - user.setEmail(oAuth2UserInfo.getEmail()); - user.setProfileUrl(oAuth2UserInfo.getImageUrl()); + User user = new User(null, oAuth2UserInfo.getEmail(), null, oAuth2UserInfo.getName(), oAuth2UserInfo.getImageUrl(), null, null, AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()), oAuth2UserInfo.getId()); return userRepository.save(user); } private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) { - existingUser.setNickname(oAuth2UserInfo.getName()); - existingUser.setProfileUrl(oAuth2UserInfo.getImageUrl()); + existingUser.changeNickname(oAuth2UserInfo.getName()); + existingUser.changeProfileUrl(oAuth2UserInfo.getImageUrl()); return userRepository.save(existingUser); } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java index e5721a2..54a88a4 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/OAuth2AuthenticationSuccessHandler.java @@ -1,15 +1,16 @@ package com.ddd.moodof.adapter.infrastructure.security.oauth2; import com.ddd.moodof.adapter.infrastructure.configuration.AppProperties; -import com.ddd.moodof.adapter.infrastructure.auth.exception.BadRequestException; -import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; -import org.springframework.beans.factory.annotation.Autowired; +import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; +import com.ddd.moodof.adapter.infrastructure.security.exception.BadRequestException; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.ServletException; + import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -19,26 +20,15 @@ import static com.ddd.moodof.adapter.infrastructure.security.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; +@RequiredArgsConstructor @Component public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - - private TokenProvider tokenProvider; - - private AppProperties appProperties; - - private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; - - - @Autowired - OAuth2AuthenticationSuccessHandler(TokenProvider tokenProvider, AppProperties appProperties, - HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository) { - this.tokenProvider = tokenProvider; - this.appProperties = appProperties; - this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; - } + private final TokenProvider tokenProvider; + private final AppProperties appProperties; + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String targetUrl = determineTargetUrl(request, response, authentication); if (response.isCommitted()) { @@ -54,13 +44,14 @@ protected String determineTargetUrl(HttpServletRequest request, HttpServletRespo Optional redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) .map(Cookie::getValue); - if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) { + if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) { throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication"); } String targetUrl = redirectUri.orElse(getDefaultTargetUrl()); - String token = tokenProvider.createToken(authentication); + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + String token = tokenProvider.createToken(principal.getId()); return UriComponentsBuilder.fromUriString(targetUrl) .queryParam("token", token) @@ -80,11 +71,8 @@ private boolean isAuthorizedRedirectUri(String uri) { .anyMatch(authorizedRedirectUri -> { // Only validate host and port. Let the clients use different paths if they want to URI authorizedURI = URI.create(authorizedRedirectUri); - if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) - && authorizedURI.getPort() == clientRedirectUri.getPort()) { - return true; - } - return false; + return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost()) + && authorizedURI.getPort() == clientRedirectUri.getPort(); }); } } diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java index 0c9345f..5462d71 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/oauth2/user/OAuth2UserInfoFactory.java @@ -1,6 +1,6 @@ package com.ddd.moodof.adapter.infrastructure.security.oauth2.user; -import com.ddd.moodof.adapter.infrastructure.auth.exception.OAuth2AuthenticationProcessingException; +import com.ddd.moodof.adapter.infrastructure.security.exception.OAuth2AuthenticationProcessingException; import com.ddd.moodof.domain.model.user.AuthProvider; import java.util.Map; @@ -8,18 +8,9 @@ public class OAuth2UserInfoFactory { public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { - if(registrationId.equalsIgnoreCase(AuthProvider.google.toString())) { + if (registrationId.equalsIgnoreCase(AuthProvider.google.toString())) { return new GoogleOAuth2UserInfo(attributes); } - /* - else if (registrationId.equalsIgnoreCase(AuthProvider.facebook.toString())) { - return new FacebookOAuth2UserInfo(attributes); - } else if (registrationId.equalsIgnoreCase(AuthProvider.github.toString())) { - return new GithubOAuth2UserInfo(attributes); - } - */ - else { - throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); - } + throw new OAuth2AuthenticationProcessingException("Sorry! Login with " + registrationId + " is not supported yet."); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java b/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java index 4ce08cd..4f4b200 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java @@ -1,5 +1,6 @@ package com.ddd.moodof.adapter.presentation; +import com.ddd.moodof.adapter.presentation.api.StoragePhotoAPI; import com.ddd.moodof.application.StoragePhotoService; import com.ddd.moodof.application.dto.StoragePhotoDTO; import lombok.RequiredArgsConstructor; @@ -15,14 +16,15 @@ @RequiredArgsConstructor @RequestMapping(StoragePhotoController.API_STORAGE_PHOTO) @RestController -public class StoragePhotoController { +public class StoragePhotoController implements StoragePhotoAPI { public static final String API_STORAGE_PHOTO = "/api/storage-photos"; private final StoragePhotoService storagePhotoService; + @Override @PostMapping - public ResponseEntity create(@RequestBody @Valid StoragePhotoDTO.Create request, @LoginUserId Long userId) { - StoragePhotoDTO.Response response = storagePhotoService.create(request, userId); + public ResponseEntity create(@RequestBody @Valid StoragePhotoDTO.CreateStoragePhoto request, @LoginUserId Long userId) { + StoragePhotoDTO.StoragePhotoResponse response = storagePhotoService.create(request, userId); return ResponseEntity.created(URI.create(API_STORAGE_PHOTO + "/" + response.getId())).body(response); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java b/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java index e578cfe..b19ee9c 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/UserController.java @@ -1,36 +1,23 @@ package com.ddd.moodof.adapter.presentation; -import com.ddd.moodof.adapter.infrastructure.auth.exception.ResourceNotFoundException; -import com.ddd.moodof.domain.model.user.UserRepository; -import com.ddd.moodof.adapter.infrastructure.security.CurrentUser; -import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; -import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; -import com.ddd.moodof.domain.model.user.User; +import com.ddd.moodof.adapter.presentation.api.UserAPI; +import com.ddd.moodof.application.UserService; +import com.ddd.moodof.application.dto.UserDTO; import lombok.RequiredArgsConstructor; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -public class UserController { - private final UserRepository userRepository; - private final TokenProvider tokenProvider; +public class UserController implements UserAPI { - @GetMapping("/user/me") - @PreAuthorize("hasRole('USER')") - public User getCurrentUser(@CurrentUser UserPrincipal userPrincipal) { - String token = tokenProvider.getToken(); - System.err.println("token = " + token); - return userRepository.findById(userPrincipal.getId()) - .orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId())); - } - @GetMapping("/user/token") - public String getUserToken(@AuthenticationPrincipal User user, @RequestHeader("Authorization") String authorization) { - System.err.println("authorization = " + authorization); - System.err.println("user = " + user); - return authorization; + private final UserService userService; + + @Override + @GetMapping("/api/me") + public ResponseEntity findMe(@LoginUserId Long id) { + UserDTO.UserResponse response = userService.findById(id); + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java new file mode 100644 index 0000000..92b88f7 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java @@ -0,0 +1,17 @@ +package com.ddd.moodof.adapter.presentation.api; + +import com.ddd.moodof.adapter.presentation.LoginUserId; +import com.ddd.moodof.application.dto.StoragePhotoDTO; +import io.swagger.annotations.ApiImplicitParam; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import springfox.documentation.annotations.ApiIgnore; + +import javax.validation.Valid; + +public interface StoragePhotoAPI { + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") + @PostMapping + ResponseEntity create(@RequestBody @Valid StoragePhotoDTO.CreateStoragePhoto request, @ApiIgnore @LoginUserId Long userId); +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/UserAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/UserAPI.java new file mode 100644 index 0000000..946aa3d --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/UserAPI.java @@ -0,0 +1,14 @@ +package com.ddd.moodof.adapter.presentation.api; + +import com.ddd.moodof.adapter.presentation.LoginUserId; +import com.ddd.moodof.application.dto.UserDTO; +import io.swagger.annotations.ApiImplicitParam; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import springfox.documentation.annotations.ApiIgnore; + +public interface UserAPI { + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") + @GetMapping("/api/me") + ResponseEntity findMe(@ApiIgnore @LoginUserId Long id); +} diff --git a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java index d17dea7..5259c9e 100644 --- a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java +++ b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java @@ -11,9 +11,9 @@ public class StoragePhotoService { private final StoragePhotoRepository storagePhotoRepository; - public StoragePhotoDTO.Response create(StoragePhotoDTO.Create request, Long userId) { + public StoragePhotoDTO.StoragePhotoResponse create(StoragePhotoDTO.CreateStoragePhoto request, Long userId) { StoragePhoto storagePhoto = request.toEntity(userId); StoragePhoto saved = storagePhotoRepository.save(storagePhoto); - return StoragePhotoDTO.Response.from(saved); + return StoragePhotoDTO.StoragePhotoResponse.from(saved); } } diff --git a/src/main/java/com/ddd/moodof/application/UserService.java b/src/main/java/com/ddd/moodof/application/UserService.java new file mode 100644 index 0000000..298eeb2 --- /dev/null +++ b/src/main/java/com/ddd/moodof/application/UserService.java @@ -0,0 +1,17 @@ +package com.ddd.moodof.application; + +import com.ddd.moodof.application.dto.UserDTO; +import com.ddd.moodof.domain.model.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class UserService { + private final UserRepository userRepository; + + public UserDTO.UserResponse findById(Long id) { + return UserDTO.UserResponse.from(userRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 user"))); + } +} diff --git a/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java b/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java index 8d6185e..682d360 100644 --- a/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java @@ -13,7 +13,7 @@ public class StoragePhotoDTO { @NoArgsConstructor @Getter @AllArgsConstructor - public static class Create { + public static class CreateStoragePhoto { @NotBlank private String uri; @NotBlank @@ -27,7 +27,7 @@ public StoragePhoto toEntity(Long userId) { @NoArgsConstructor @Getter @AllArgsConstructor - public static class Response { + public static class StoragePhotoResponse { private Long id; private Long userId; private String uri; @@ -35,8 +35,8 @@ public static class Response { private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; - public static Response from(StoragePhoto storagePhoto) { - return new Response(storagePhoto.getId(), storagePhoto.getUserId(), storagePhoto.getUri(), storagePhoto.getRepresentativeColor(), storagePhoto.getCreatedDate(), storagePhoto.getLastModifiedDate()); + public static StoragePhotoResponse from(StoragePhoto storagePhoto) { + return new StoragePhotoResponse(storagePhoto.getId(), storagePhoto.getUserId(), storagePhoto.getUri(), storagePhoto.getRepresentativeColor(), storagePhoto.getCreatedDate(), storagePhoto.getLastModifiedDate()); } } } diff --git a/src/main/java/com/ddd/moodof/application/dto/UserDTO.java b/src/main/java/com/ddd/moodof/application/dto/UserDTO.java new file mode 100644 index 0000000..80ca8e4 --- /dev/null +++ b/src/main/java/com/ddd/moodof/application/dto/UserDTO.java @@ -0,0 +1,28 @@ +package com.ddd.moodof.application.dto; + +import com.ddd.moodof.domain.model.user.User; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + + +public class UserDTO { + + @NoArgsConstructor + @Getter + @AllArgsConstructor + public static class UserResponse { + private Long id; + private String email; + private String nickname; + private String profileUrl; + private LocalDateTime createdDate; + private LocalDateTime lastModifiedDate; + + public static UserResponse from(User user) { + return new UserResponse(user.getId(), user.getEmail(), user.getNickname(), user.getProfileUrl(), user.getCreatedDate(), user.getLastModifiedDate()); + } + } +} diff --git a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhoto.java b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhoto.java index 532248f..3c3b30f 100644 --- a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhoto.java +++ b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhoto.java @@ -20,11 +20,16 @@ public class StoragePhoto { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private Long userId; + private String uri; + private String representativeColor; + @CreatedDate private LocalDateTime createdDate; + @LastModifiedDate private LocalDateTime lastModifiedDate; } diff --git a/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java b/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java index 3d26645..8d99694 100644 --- a/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java +++ b/src/main/java/com/ddd/moodof/domain/model/user/AuthProvider.java @@ -1,5 +1,5 @@ package com.ddd.moodof.domain.model.user; public enum AuthProvider { - google + google; } diff --git a/src/main/java/com/ddd/moodof/domain/model/user/User.java b/src/main/java/com/ddd/moodof/domain/model/user/User.java index 35eacce..6add79d 100644 --- a/src/main/java/com/ddd/moodof/domain/model/user/User.java +++ b/src/main/java/com/ddd/moodof/domain/model/user/User.java @@ -1,43 +1,55 @@ package com.ddd.moodof.domain.model.user; -import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import javax.validation.constraints.Email; import javax.validation.constraints.NotNull; import java.time.LocalDateTime; -@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -@Setter -@NoArgsConstructor +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +@Entity public class User { - @Id @GeneratedValue - @Column(name = "user_id") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Email @Column(nullable = false) private String email; - @JsonIgnore private String password; private String nickname; private String profileUrl; - private LocalDateTime createdTime; - - private LocalDateTime modifiedTime; + @CreatedDate + private LocalDateTime createdDate; + @LastModifiedDate + private LocalDateTime lastModifiedDate; @NotNull @Enumerated(EnumType.STRING) private AuthProvider provider; private String providerId; + + public void changeNickname(String nickname) { + this.nickname = nickname; + } + + public void changeProfileUrl(String profileUrl) { + this.profileUrl = profileUrl; + } } diff --git a/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java b/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java index 5e570a8..efa6e4c 100644 --- a/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/user/UserRepository.java @@ -1,14 +1,12 @@ package com.ddd.moodof.domain.model.user; -import com.ddd.moodof.domain.model.user.User; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - - Boolean existsByEmail(String email); - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 61bd60b..9438375 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: local + include: security datasource: url: jdbc:h2:mem:test username: sa @@ -18,35 +18,6 @@ spring: enabled: true settings: web-allow-others: true - security: - oauth2: - client: - registration: - google: - clientId: 816491450432-8a14tsegerl77m5nul14pa1q0fo3mb5a.apps.googleusercontent.com - clientSecret: mRHdZmxmoae_ow32LSmZGrZk - redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" - scope: - - email - - profile -app: - auth: - tokenSecret: 926D96C90030DD58429D2751AC1BDBBC - tokenExpirationMsec: 84000000 - # 60000:60초 - oauth2: - # After successfully authenticating with the OAuth2 Provider, - # we'll be generating an auth token for the user and sending the token to the - # redirectUri mentioned by the client in the /oauth2/authorize request. - # We're not using cookies because they won't work well in mobile clients. - authorizedRedirectUris: - - http://localhost:3000/oauth2/redirect - - myandroidapp://oauth2/redirect - - myiosapp://oauth2/redirect logging.level: org.hibernate.SQL: debug -# org.hibernate.type: trace - -#apply spring security - diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index 6791198..792d4fb 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -1,5 +1,9 @@ package com.ddd.moodof.acceptance; +import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.domain.model.user.AuthProvider; +import com.ddd.moodof.domain.model.user.User; +import com.ddd.moodof.domain.model.user.UserRepository; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.hamcrest.Matchers; @@ -32,6 +36,12 @@ public class AcceptanceTest { @Autowired private WebApplicationContext context; + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenProvider tokenProvider; + @BeforeEach void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(context) @@ -40,11 +50,9 @@ void setUp() { .build(); } - protected U postWithLogin(T request, String uri, Class response, Long userId) { try { - // TODO: 2021/04/06 인증 인가 완료시 토큰으로 변경 (TokenProvider) - String token = userId.toString(); + String token = tokenProvider.createToken(userId); String body = objectMapper.writeValueAsString(request); MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post(uri) @@ -53,7 +61,29 @@ protected U postWithLogin(T request, String uri, Class response, Long .content(body) .header(AUTHORIZATION, BEARER + token)) .andExpect(MockMvcResultMatchers.status().isCreated()) - .andExpect(MockMvcResultMatchers.header().string(HttpHeaders.LOCATION, Matchers.matchesRegex(uri + "/com.ddd.moodof.authorization\\d*"))) + .andExpect(MockMvcResultMatchers.header().string(HttpHeaders.LOCATION, Matchers.matchesRegex(uri + "/\\d*"))) + .andReturn(); + + return objectMapper.readValue(result.getResponse().getContentAsString(), response); + } catch (Exception e) { + log.error(e.getMessage()); + throw new AssertionError("test fails"); + } + } + + protected User signUp() { + return userRepository.save(new User(null, "test@test.com", "password", "nickname", "profileUrl", null, null, AuthProvider.google, "providerId")); + } + + protected T getWithLogin(String uri, Class response, Long userId) { + try { + + String token = tokenProvider.createToken(userId); + + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri) + .contentType(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION, BEARER + token)) + .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); return objectMapper.readValue(result.getResponse().getContentAsString(), response); diff --git a/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java b/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java deleted file mode 100644 index 371db10..0000000 --- a/src/test/java/com/ddd/moodof/acceptance/AuthorizationTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.ddd.moodof.acceptance; - -import com.ddd.moodof.domain.model.user.AuthProvider; -import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; -import com.ddd.moodof.adapter.infrastructure.security.UserPrincipal; -import com.ddd.moodof.adapter.infrastructure.configuration.CookieUtils; -import com.ddd.moodof.domain.model.user.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@AutoConfigureMockMvc -@SpringBootTest -public class AuthorizationTest { - public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; - public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; - private static final int cookieExpireSeconds = 3600; - - @Autowired - private MockMvc mockMvc; - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private WebApplicationContext webappContext; - - @BeforeEach - public void init() { - mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) - .apply(springSecurity()) - .build(); - } - @Test - @WithMockUser(username = "moodof ddd",password = "", authorities = "USER") - public void JWT_토큰_생성() throws Exception { - OAuth2User oAuth2User = Mockito.mock(OAuth2User.class); - Authentication authorizationRequest = Mockito.mock(Authentication.class); - UserPrincipal userPrincipal = mock(UserPrincipal.class); - User user = new User(); - user.setEmail("ddd.moodof@gmail.com"); - user.setNickname("moodof ddd"); - user.setProfileUrl("https://lh6.googleusercontent.com/-4vjsYgdYzVE/AAAAAAAAAAI/AAAAAAAAAAA/AMZuuckpFmWzV5cRiDCebObd-AidXzUC8g/s96-c/photo.jpg"); - user.setProvider(AuthProvider.google); - user.setProviderId("107406868053916247309"); - - userPrincipal.create(user, oAuth2User.getAttributes()); - SecurityContext securityContext = mock(SecurityContext.class); - SecurityContextHolder.setContext(securityContext); - - // Mockito.whens() for your authorization object - when(securityContext.getAuthentication()).thenReturn(authorizationRequest); - when(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).thenReturn(userPrincipal); - when(securityContext.getAuthentication()).thenReturn(authorizationRequest); - System.err.println("authorizationRequest.getPrincipal() = " + authorizationRequest.getPrincipal()); - - /* - JWT 토큰 생성후 값 받아오기 - */ - tokenProvider.createToken(authorizationRequest); - String token = tokenProvider.getToken(); - - /* - JWT 토큰처리 - */ - System.out.println("token = " + token); - mockMvc.perform(RequestSecurityFactory.securityFactory("/user/token", token) - .header(AUTHORIZATION, "Bearer " + token)) - .andExpect(status().isOk());; - - } - @Test - @WithMockUser(username = "moodof ddd",password = "", authorities = "USER") - public void 토큰_쿠키_생성() throws Exception { - Authentication authorizationCookieRequest = Mockito.mock(Authentication.class); - SecurityContext securityContext = mock(SecurityContext.class); - SecurityContextHolder.setContext(securityContext); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - if (authorizationCookieRequest == null) { - CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); - CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); - } - Cookie cookie = CookieUtils.addCookieTest(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationCookieRequest), cookieExpireSeconds); - request.setAttribute(REDIRECT_URI_PARAM_COOKIE_NAME, cookie.getValue()); - String token = cookie.getValue(); - System.out.println("token = " + token); - mockMvc.perform(RequestSecurityFactory.securityFactory("/user/token", token) - .header(AUTHORIZATION, "Bearer " + token)) - .andExpect(status().isOk()); - } - - /** - * 인증 토큰값으로 인증가능 - * @throws Exception - */ - @Test - void 토큰_인증_헤더() throws Exception { - String token = ""; - System.err.println(token); - mockMvc.perform(MockMvcRequestBuilders.get("/user/token") - .header(AUTHORIZATION, "Bearer " + token)) - .andExpect(status().isOk()); - } - - /** - * 시큐리티 클라이언트 헤더값 세팅 - */ - public static class RequestSecurityFactory { - /** - * 요청값 헤더값 세팅 - * @param url - * @param token - * @return - */ - public static MockHttpServletRequestBuilder securityFactory(String url, String token) { - return MockMvcRequestBuilders.get(url) - .header(AUTHORIZATION, "Bearer " + token); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java index 979726d..c7bf281 100644 --- a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java @@ -1,6 +1,7 @@ package com.ddd.moodof.acceptance; import com.ddd.moodof.application.dto.StoragePhotoDTO; +import com.ddd.moodof.domain.model.user.User; import org.junit.jupiter.api.Test; import static com.ddd.moodof.adapter.presentation.StoragePhotoController.API_STORAGE_PHOTO; @@ -10,13 +11,13 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { @Test void 사진보관함에_사진을_추가한다() { - // TODO: 2021/04/06 인증 인가 완료시 회원 가입 후 userId를 응답받아야 한다. // given - Long userId = 1L; + User user = signUp(); + Long userId = user.getId(); // when - StoragePhotoDTO.Create request = new StoragePhotoDTO.Create("uri", "representativeColor"); - StoragePhotoDTO.Response response = postWithLogin(request, API_STORAGE_PHOTO, StoragePhotoDTO.Response.class, userId); + StoragePhotoDTO.CreateStoragePhoto request = new StoragePhotoDTO.CreateStoragePhoto("uri", "representativeColor"); + StoragePhotoDTO.StoragePhotoResponse response = postWithLogin(request, API_STORAGE_PHOTO, StoragePhotoDTO.StoragePhotoResponse.class, userId); // then assertAll( diff --git a/src/test/java/com/ddd/moodof/acceptance/UserAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/UserAcceptanceTest.java new file mode 100644 index 0000000..d471939 --- /dev/null +++ b/src/test/java/com/ddd/moodof/acceptance/UserAcceptanceTest.java @@ -0,0 +1,30 @@ +package com.ddd.moodof.acceptance; + +import com.ddd.moodof.application.dto.UserDTO; +import com.ddd.moodof.domain.model.user.User; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class UserAcceptanceTest extends AcceptanceTest { + + @Test + void 자신의_정보를_조회한다() { + // given + User user = signUp(); + + // when + UserDTO.UserResponse response = getWithLogin("/api/me", UserDTO.UserResponse.class, user.getId()); + + // then + assertAll( + () -> assertThat(response.getId()).isEqualTo(user.getId()), + () -> assertThat(response.getEmail()).isEqualTo(user.getEmail()), + () -> assertThat(response.getNickname()).isEqualTo(user.getNickname()), + () -> assertThat(response.getProfileUrl()).isEqualTo(user.getProfileUrl()), + () -> assertThat(response.getCreatedDate()).isEqualTo(user.getCreatedDate()), + () -> assertThat(response.getLastModifiedDate()).isEqualTo(user.getLastModifiedDate()) + ); + } +} From 78fdaa840d5f6861360a4aee77044eacaeb69ac0 Mon Sep 17 00:00:00 2001 From: Gyeongjun Kim Date: Sat, 22 May 2021 18:18:37 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=83=9C=EA=B7=B8=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=97=86=EC=9D=8C=20=EA=B2=80=EC=83=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StoragePhotoQuerydslRepository.java | 64 +++++++++++++++---- .../presentation/StoragePhotoController.java | 7 +- .../presentation/api/StoragePhotoAPI.java | 2 +- .../application/StoragePhotoService.java | 4 +- .../photo/StoragePhotoQueryRepository.java | 2 +- .../StoragePhotoAcceptanceTest.java | 6 +- 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java index 6bf9ed1..7956fb1 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java @@ -35,6 +35,8 @@ @RequiredArgsConstructor @Repository public class StoragePhotoQuerydslRepository implements StoragePhotoQueryRepository { + private static final long NO_TAG = 0L; + private final EntityManager em; private final JPAQueryFactory jpaQueryFactory; private final PaginationUtils paginationUtils; @@ -70,8 +72,13 @@ private JPAQuery getQuery(Long userId, Lis return storagePhotoQuery .where(excludeTrash); } + if (tagIds.contains(NO_TAG)) { + return storagePhotoQuery + .leftJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .where(excludeTrash.and(tagAttachment.tagId.in(tagIds).or(tagAttachment.id.isNull()))); + } return storagePhotoQuery - .leftJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .innerJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) .where(excludeTrash.and(tagAttachment.tagId.in(tagIds))); } @@ -80,7 +87,7 @@ private BooleanExpression excludeTrash(Long userId) { } @Override - public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id) { + public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id, List tagIds) { StoragePhotoDTO.StoragePhotoDetailResponse storagePhotoDetailResponse = jpaQueryFactory .select(new QStoragePhotoDTO_StoragePhotoDetailResponse( storagePhoto.id, @@ -89,8 +96,8 @@ public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long i storagePhoto.representativeColor, storagePhoto.createdDate, storagePhoto.lastModifiedDate, - ExpressionUtils.as(previousStoragePhotoId(userId, id), "previousStoragePhotoId"), - ExpressionUtils.as(nextStoragePhotoId(userId, id), "nextStoragePhotoId"))) + ExpressionUtils.as(previousStoragePhotoId(userId, id, tagIds), "previousStoragePhotoId"), + ExpressionUtils.as(nextStoragePhotoId(userId, id, tagIds), "nextStoragePhotoId"))) .from(storagePhoto) .where(storagePhoto.id.eq(id)) .fetchOne(); @@ -101,19 +108,36 @@ public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long i return storagePhotoDetailResponse; } - private JPQLQuery previousStoragePhotoId(Long userId, Long id) { + private JPQLQuery previousStoragePhotoId(Long userId, Long id, List tagIds) { return JPAExpressions .select(storagePhoto.id) .from(storagePhoto) - .where(storagePhoto.userId.eq(userId).and(storagePhoto.lastModifiedDate.eq(lastModifiedDateAfterStoragePhoto(userId, id)))); + .where(storagePhoto.userId.eq(userId).and(storagePhoto.lastModifiedDate.eq(lastModifiedDateAfterStoragePhoto(userId, id, tagIds)))); } - private JPQLQuery lastModifiedDateAfterStoragePhoto(Long userId, Long id) { + private JPQLQuery lastModifiedDateAfterStoragePhoto(Long userId, Long id, List tagIds) { + if (tagIds.isEmpty()) { + return JPAExpressions + .select(storagePhoto.lastModifiedDate.min()) + .from(storagePhoto) + .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) + .where(excludeTrash(userId).and(storagePhoto.lastModifiedDate.after(storagePhotoModifiedDate(id)))); + } + if (tagIds.contains(NO_TAG)) { + return JPAExpressions + .select(storagePhoto.lastModifiedDate.min()) + .from(storagePhoto) + .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) + .leftJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .where(excludeTrash(userId).and(tagAttachment.tagId.in(tagIds).or(tagAttachment.id.isNull())).and(storagePhoto.lastModifiedDate.after(storagePhotoModifiedDate(id)))); + } + return JPAExpressions .select(storagePhoto.lastModifiedDate.min()) .from(storagePhoto) .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) - .where(excludeTrash(userId).and(storagePhoto.lastModifiedDate.after(storagePhotoModifiedDate(id)))); + .innerJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .where(excludeTrash(userId).and(tagAttachment.tagId.in(tagIds)).and(storagePhoto.lastModifiedDate.after(storagePhotoModifiedDate(id)))); } private JPQLQuery storagePhotoModifiedDate(Long id) { @@ -123,19 +147,35 @@ private JPQLQuery storagePhotoModifiedDate(Long id) { .where(storagePhoto.id.eq(id)); } - private JPQLQuery nextStoragePhotoId(Long userId, Long id) { + private JPQLQuery nextStoragePhotoId(Long userId, Long id, List tagIds) { return JPAExpressions .select(storagePhoto.id) .from(storagePhoto) - .where(storagePhoto.userId.eq(userId).and(storagePhoto.lastModifiedDate.eq(lastModifiedDateBeforeStoragePhoto(userId, id)))); + .where(storagePhoto.userId.eq(userId).and(storagePhoto.lastModifiedDate.eq(lastModifiedDateBeforeStoragePhoto(userId, id, tagIds)))); } - private JPQLQuery lastModifiedDateBeforeStoragePhoto(Long userId, Long id) { + private JPQLQuery lastModifiedDateBeforeStoragePhoto(Long userId, Long id, List tagIds) { + if (tagIds.isEmpty()) { + return JPAExpressions + .select(storagePhoto.lastModifiedDate.max()) + .from(storagePhoto) + .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) + .where(excludeTrash(userId).and(storagePhoto.lastModifiedDate.before(storagePhotoModifiedDate(id)))); + } + if (tagIds.contains(NO_TAG)) { + return JPAExpressions + .select(storagePhoto.lastModifiedDate.max()) + .from(storagePhoto) + .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) + .leftJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .where(excludeTrash(userId).and(tagAttachment.tagId.in(tagIds).or(tagAttachment.id.isNull())).and(storagePhoto.lastModifiedDate.before(storagePhotoModifiedDate(id)))); + } return JPAExpressions .select(storagePhoto.lastModifiedDate.max()) .from(storagePhoto) .leftJoin(trashPhoto).on(storagePhoto.id.eq(trashPhoto.storagePhotoId)) - .where(excludeTrash(userId).and(storagePhoto.lastModifiedDate.before(storagePhotoModifiedDate(id)))); + .innerJoin(tagAttachment).on(storagePhoto.id.eq(tagAttachment.storagePhotoId)) + .where(excludeTrash(userId).and(tagAttachment.tagId.in(tagIds)).and(storagePhoto.lastModifiedDate.before(storagePhotoModifiedDate(id)))); } private List findCategories(Long storagePhotoId) { diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java b/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java index d4955df..dafa254 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/StoragePhotoController.java @@ -46,8 +46,11 @@ public ResponseEntity findPage( @Override @GetMapping("/{id}") - public ResponseEntity findDetail(@LoginUserId Long userId, @PathVariable Long id) { - StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findDetail(userId, id); + public ResponseEntity findDetail( + @LoginUserId Long userId, + @PathVariable Long id, + @RequestParam(required = false, value = "tagIds") List tagIds) { + StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findDetail(userId, id, tagIds); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java index 4d718ca..7bc2772 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/StoragePhotoAPI.java @@ -28,7 +28,7 @@ ResponseEntity findPage( @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @GetMapping("/{id}") - ResponseEntity findDetail(@ApiIgnore @LoginUserId Long userId, @PathVariable Long id); + ResponseEntity findDetail(@ApiIgnore @LoginUserId Long userId, @PathVariable Long id, @RequestParam(required = false, value = "tagIds[]") List tagIds); @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @DeleteMapping("/{id}") diff --git a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java index 2c58c92..13c7c09 100644 --- a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java +++ b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java @@ -40,7 +40,7 @@ public void deleteById(Long userId, Long id) { storagePhotoRepository.deleteById(id); } - public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id) { - return storagePhotoQueryRepository.findDetail(userId, id); + public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id, List tagIds) { + return storagePhotoQueryRepository.findDetail(userId, id, tagIds); } } diff --git a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoQueryRepository.java b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoQueryRepository.java index 3e67869..7fc4f65 100644 --- a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoQueryRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoQueryRepository.java @@ -8,5 +8,5 @@ public interface StoragePhotoQueryRepository { StoragePhotoDTO.StoragePhotoPageResponse findPageExcludeTrash(Long userId, Pageable pageable, List tagIds); - StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id); + StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id, List tagIds); } diff --git a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java index ef9485b..39edac8 100644 --- a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java @@ -94,6 +94,7 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { @Test void 사진보관함_페이지_태그별_조회() { // given + StoragePhotoDTO.StoragePhotoResponse noTag = 보관함사진_생성(userId, "0", "0"); StoragePhotoDTO.StoragePhotoResponse third = 보관함사진_생성(userId, "1", "1"); StoragePhotoDTO.StoragePhotoResponse noContain = 보관함사진_생성(userId, "2", "2"); StoragePhotoDTO.StoragePhotoResponse second = 보관함사진_생성(userId, "3", "3"); @@ -115,7 +116,7 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { .queryParam("size", 2) .queryParam("sortBy", "lastModifiedDate") .queryParam("descending", "true") - .queryParam("tagIds", tag1.getId(), tag2.getId()) + .queryParam("tagIds", 0L, tag1.getId(), tag2.getId()) .build().toUriString(); StoragePhotoDTO.StoragePhotoPageResponse response = getWithLogin(uri, StoragePhotoDTO.StoragePhotoPageResponse.class, userId); @@ -161,10 +162,11 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { TagDTO.TagResponse tag2 = 태그_생성(userId, "tag-2"); 태그붙이기_생성(userId, storagePhoto2.getId(), tag1.getId()); 태그붙이기_생성(userId, storagePhoto2.getId(), tag2.getId()); + 태그붙이기_생성(userId, storagePhoto5.getId(), tag2.getId()); 보관함사진_휴지통_이동(List.of(storagePhoto3.getId(), storagePhoto4.getId()), userId); // when - StoragePhotoDTO.StoragePhotoDetailResponse response = getWithLogin(API_STORAGE_PHOTO + "/" + storagePhoto2.getId(), StoragePhotoDTO.StoragePhotoDetailResponse.class, userId); + StoragePhotoDTO.StoragePhotoDetailResponse response = getWithLogin(API_STORAGE_PHOTO + "/" + storagePhoto2.getId() + "?tagIds=0," + tag2.getId(), StoragePhotoDTO.StoragePhotoDetailResponse.class, userId); // then assertAll( From 72ce0703e763f5bebb6fad54867dacc884f14f2d Mon Sep 17 00:00:00 2001 From: Gyeongjun Kim Date: Tue, 25 May 2021 22:23:09 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20BoardPhoto=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/BoardPhotoController.java | 14 +++---- .../presentation/api/BoardPhotoAPI.java | 10 +++-- .../moodof/application/BoardPhotoService.java | 41 +++++++++++++++---- .../moodof/application/dto/BoardPhotoDTO.java | 20 ++++++++- .../moodof/application/dto/CategoryDTO.java | 11 +++-- .../verifier/BoardPhotoVerifier.java | 17 ++++++-- .../domain/model/board/photo/BoardPhoto.java | 6 +++ .../board/photo/BoardPhotoRepository.java | 3 ++ .../storage/photo/StoragePhotoRepository.java | 5 ++- .../ddd/moodof/acceptance/AcceptanceTest.java | 29 ++++++++++--- .../acceptance/BoardPhotoAcceptanceTest.java | 26 ++++++++---- .../StoragePhotoAcceptanceTest.java | 9 ++-- 12 files changed, 137 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java b/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java index e92f532..f337820 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java @@ -7,7 +7,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; +import java.util.List; @RequiredArgsConstructor @@ -20,15 +20,15 @@ public class BoardPhotoController implements BoardPhotoAPI { @Override @PostMapping - public ResponseEntity addPhoto(@LoginUserId Long userId, @RequestBody BoardPhotoDTO.AddBoardPhoto request) { - BoardPhotoDTO.BoardPhotoResponse response = boardPhotoService.addPhoto(userId, request); - return ResponseEntity.created(URI.create(API_BOARD_PHOTO + "/" + response.getId())).body(response); + public ResponseEntity> addPhotos(@LoginUserId Long userId, @RequestBody BoardPhotoDTO.AddBoardPhoto request) { + List responses = boardPhotoService.addPhotos(userId, request); + return ResponseEntity.ok(responses); } @Override - @DeleteMapping("/{id}") - public ResponseEntity removePhoto(@LoginUserId Long userId, @PathVariable Long id) { - boardPhotoService.removePhoto(userId, id); + @DeleteMapping + public ResponseEntity removePhoto(@LoginUserId Long userId, @RequestBody BoardPhotoDTO.RemoveBoardPhotos request) { + boardPhotoService.removePhoto(userId, request); return ResponseEntity.noContent().build(); } } \ No newline at end of file diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java index 634608b..0459b2c 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java @@ -1,20 +1,22 @@ package com.ddd.moodof.adapter.presentation.api; +import com.ddd.moodof.adapter.presentation.LoginUserId; import com.ddd.moodof.application.dto.BoardPhotoDTO; import io.swagger.annotations.ApiImplicitParam; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import springfox.documentation.annotations.ApiIgnore; +import java.util.List; + public interface BoardPhotoAPI { @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @PostMapping - ResponseEntity addPhoto(@ApiIgnore Long userId, @RequestBody BoardPhotoDTO.AddBoardPhoto request); + ResponseEntity> addPhotos(@ApiIgnore Long userId, @RequestBody BoardPhotoDTO.AddBoardPhoto request); @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") - @DeleteMapping("/{id}") - ResponseEntity removePhoto(@ApiIgnore Long userId, @PathVariable Long id); + @DeleteMapping + ResponseEntity removePhoto(@ApiIgnore @LoginUserId Long userId, @RequestBody BoardPhotoDTO.RemoveBoardPhotos request); } diff --git a/src/main/java/com/ddd/moodof/application/BoardPhotoService.java b/src/main/java/com/ddd/moodof/application/BoardPhotoService.java index a6aa3fa..71f8237 100644 --- a/src/main/java/com/ddd/moodof/application/BoardPhotoService.java +++ b/src/main/java/com/ddd/moodof/application/BoardPhotoService.java @@ -7,26 +7,49 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + @RequiredArgsConstructor @Service public class BoardPhotoService { + private static final long FIRST_PREVIOUS_BOARD_PHOTO_ID = 0L; + private final BoardPhotoRepository boardPhotoRepository; private final BoardPhotoVerifier boardPhotoVerifier; - public BoardPhotoDTO.BoardPhotoResponse addPhoto(Long userId, BoardPhotoDTO.AddBoardPhoto request) { - BoardPhoto boardPhoto = boardPhotoVerifier.toEntity(userId, request.getStoragePhotoId(), request.getBoardId()); - BoardPhoto saved = boardPhotoRepository.save(boardPhoto); - return BoardPhotoDTO.BoardPhotoResponse.from(saved); + public List addPhotos(Long userId, BoardPhotoDTO.AddBoardPhoto request) { + // TODO: 2021/05/25 StoragePhoto 삭제 대응 + List boardPhotos = boardPhotoVerifier.toEntities(userId, request.getStoragePhotoIds(), request.getBoardId()); + Optional recentest = boardPhotoRepository.findFirstByBoardIdOrderByLastModifiedDateDesc(request.getBoardId()); + if (recentest.isPresent()) { + return BoardPhotoDTO.BoardPhotoResponse.listFrom(saveWithSequence(boardPhotos, recentest.get().getId())); + } + return BoardPhotoDTO.BoardPhotoResponse.listFrom(saveWithSequence(boardPhotos, FIRST_PREVIOUS_BOARD_PHOTO_ID)); + } + + private List saveWithSequence(List boardPhotos, Long previousId) { + List result = new ArrayList<>(); + Long previousBoardPhotoId = previousId; + + for (BoardPhoto boardPhoto : boardPhotos) { + boardPhoto.setPreviousBoardPhotoId(previousBoardPhotoId); + BoardPhoto saved = boardPhotoRepository.save(boardPhoto); + + result.add(saved); + previousBoardPhotoId = saved.getId(); + } + return result; } - public void removePhoto(Long userId, Long id) { - BoardPhoto boardPhoto = boardPhotoRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 BoardPhoto, id : " + id)); + public void removePhoto(Long userId, BoardPhotoDTO.RemoveBoardPhotos request) { + List boardPhotos = boardPhotoRepository.findAllById(request.getBoardPhotoIds()); - if (boardPhoto.isUserNotEqual(userId)) { + if (boardPhotos.stream().anyMatch(boardPhoto -> boardPhoto.isUserNotEqual(userId))) { throw new IllegalArgumentException("로그인한 유저와 BoardPhoto를 생성한 유저의 아이디가 다릅니다."); } - boardPhotoRepository.deleteById(id); + boardPhotoRepository.deleteAll(boardPhotos); } } diff --git a/src/main/java/com/ddd/moodof/application/dto/BoardPhotoDTO.java b/src/main/java/com/ddd/moodof/application/dto/BoardPhotoDTO.java index 5775ea8..9d4f37f 100644 --- a/src/main/java/com/ddd/moodof/application/dto/BoardPhotoDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/BoardPhotoDTO.java @@ -6,6 +6,8 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; public class BoardPhotoDTO { @NoArgsConstructor @@ -16,11 +18,18 @@ public static class BoardPhotoResponse { private Long storagePhotoId; private Long boardId; private Long userId; + private Long previousBoardPhotoId; private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; public static BoardPhotoResponse from(BoardPhoto boardPhoto) { - return new BoardPhotoResponse(boardPhoto.getId(), boardPhoto.getStoragePhotoId(), boardPhoto.getBoardId(), boardPhoto.getUserId(), boardPhoto.getCreatedDate(), boardPhoto.getLastModifiedDate()); + return new BoardPhotoResponse(boardPhoto.getId(), boardPhoto.getStoragePhotoId(), boardPhoto.getBoardId(), boardPhoto.getUserId(), boardPhoto.getPreviousBoardPhotoId(), boardPhoto.getCreatedDate(), boardPhoto.getLastModifiedDate()); + } + + public static List listFrom(List boardPhotos) { + return boardPhotos.stream() + .map(BoardPhotoResponse::from) + .collect(Collectors.toList()); } } @@ -28,7 +37,14 @@ public static BoardPhotoResponse from(BoardPhoto boardPhoto) { @AllArgsConstructor @Getter public static class AddBoardPhoto { - private Long storagePhotoId; + private List storagePhotoIds; private Long boardId; } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + public static class RemoveBoardPhotos { + private List boardPhotoIds; + } } diff --git a/src/main/java/com/ddd/moodof/application/dto/CategoryDTO.java b/src/main/java/com/ddd/moodof/application/dto/CategoryDTO.java index d6b91a1..3bd052b 100644 --- a/src/main/java/com/ddd/moodof/application/dto/CategoryDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/CategoryDTO.java @@ -1,6 +1,5 @@ package com.ddd.moodof.application.dto; -import com.ddd.moodof.domain.model.board.Board; import com.ddd.moodof.domain.model.category.Category; import com.querydsl.core.annotations.QueryProjection; import lombok.AllArgsConstructor; @@ -10,7 +9,6 @@ import javax.validation.constraints.NotBlank; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -44,7 +42,7 @@ public static CategoryResponse from(Category category) { .build(); } - public static List listForm(List categories) { + public static List listFrom(List categories) { return categories.stream().map(category -> CategoryResponse.builder() .id(category.getId()) .userId(category.getUserId()) @@ -61,7 +59,7 @@ public static List listForm(List categories) { @AllArgsConstructor @Getter @Builder - public static class CategoryWithBoardResponse{ + public static class CategoryWithBoardResponse { private Long id; @@ -75,7 +73,7 @@ public static class CategoryWithBoardResponse{ private LocalDateTime lastModifiedDate; - private List boardList = new ArrayList<>(); + private List boardList; public static CategoryWithBoardResponse from(CategoryDTO.CategoryResponse category, List boards) { return CategoryWithBoardResponse.builder() @@ -110,7 +108,8 @@ public Category toEntity(Long userId) { .lastModifiedDate(null) .build(); } - public Category toEntity(Long userId, String title, Long previousId){ + + public Category toEntity(Long userId, String title, Long previousId) { return Category.builder() .id(null) .previousId(previousId) diff --git a/src/main/java/com/ddd/moodof/application/verifier/BoardPhotoVerifier.java b/src/main/java/com/ddd/moodof/application/verifier/BoardPhotoVerifier.java index 624b401..b35470b 100644 --- a/src/main/java/com/ddd/moodof/application/verifier/BoardPhotoVerifier.java +++ b/src/main/java/com/ddd/moodof/application/verifier/BoardPhotoVerifier.java @@ -6,16 +6,25 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Component public class BoardPhotoVerifier { private final StoragePhotoRepository storagePhotoRepository; private final BoardRepository boardRepository; - public BoardPhoto toEntity(Long userId, Long storagePhotoId, Long boardId) { - if (storagePhotoRepository.existsByIdAndUserId(storagePhotoId, userId) && boardRepository.existsByIdAndUserId(boardId, userId)) { - return new BoardPhoto(null, storagePhotoId, boardId, userId, null, null); + public List toEntities(Long userId, List storagePhotoIds, Long boardId) { + if (storagePhotoRepository.existsByIdInAndUserId(storagePhotoIds, userId) && boardRepository.existsByIdAndUserId(boardId, userId)) { + return storagePhotoIds.stream() + .map(storagePhotoId -> new BoardPhoto(null, storagePhotoId, boardId, userId, null, null, null)) + .collect(Collectors.toList()); } - throw new IllegalArgumentException("보관함 사진 또는 보드가 존재하지 않습니다. storagePhotoId : " + storagePhotoId + ", boardId : " + boardId); + throw new IllegalArgumentException("보관함 사진 또는 보드가 존재하지 않습니다. storagePhotoIds : " + + storagePhotoIds.stream() + .map(Object::toString) + .collect(Collectors.joining(",")) + + ", boardId : " + boardId); } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhoto.java b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhoto.java index cdfa9e1..85bee66 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhoto.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhoto.java @@ -26,6 +26,8 @@ public class BoardPhoto { private Long userId; + private Long previousBoardPhotoId; + @CreatedDate private LocalDateTime createdDate; @@ -35,4 +37,8 @@ public class BoardPhoto { public boolean isUserNotEqual(Long userId) { return !this.userId.equals(userId); } + + public void setPreviousBoardPhotoId(Long previousBoardPhotoId) { + this.previousBoardPhotoId = previousBoardPhotoId; + } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java index 6fb3274..44136fd 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java @@ -2,5 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface BoardPhotoRepository extends JpaRepository { + Optional findFirstByBoardIdOrderByLastModifiedDateDesc(Long boardId); } diff --git a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoRepository.java b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoRepository.java index 963c554..fb19beb 100644 --- a/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/storage/photo/StoragePhotoRepository.java @@ -2,8 +2,11 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface StoragePhotoRepository extends JpaRepository { - boolean existsByIdAndUserId(Long id, Long userId); + boolean existsByIdInAndUserId(List ids, Long userId); + boolean existsByIdAndUserId(Long id, Long userId); } diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index e1f369a..9701ca6 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -73,12 +73,12 @@ protected User signUp() { return userRepository.save(new User(null, "test@test.com", "password", "nickname", "profileUrl", null, null, AuthProvider.google, "providerId")); } - protected CategoryDTO.CategoryResponse 카테고리_생성(Long userId, String title, Long previousId){ - CategoryDTO.CreateCategoryRequest request = new CategoryDTO.CreateCategoryRequest(title,previousId); + protected CategoryDTO.CategoryResponse 카테고리_생성(Long userId, String title, Long previousId) { + CategoryDTO.CreateCategoryRequest request = new CategoryDTO.CreateCategoryRequest(title, previousId); return postWithLogin(request, API_CATEGORY, CategoryDTO.CategoryResponse.class, userId); } - protected CategoryDTO.CategoryResponse 카테고리_순서_변경(Long id,Long previousId, Long userId, String property){ + protected CategoryDTO.CategoryResponse 카테고리_순서_변경(Long id, Long previousId, Long userId, String property) { CategoryDTO.UpdateOrderCategoryRequest request = new CategoryDTO.UpdateOrderCategoryRequest(previousId); return putPropertyWithLogin(request, id, API_CATEGORY, CategoryDTO.CategoryResponse.class, userId, property); } @@ -232,7 +232,24 @@ protected void deleteWithLogin(String uri, Long resourceId, Long userId) { String token = tokenProvider.createToken(userId); mockMvc.perform(MockMvcRequestBuilders.delete(uri + "/{id}", resourceId) + .header(AUTHORIZATION, BEARER + token)) + .andExpect(MockMvcResultMatchers.status().isNoContent()) + .andReturn(); + + } catch (Exception e) { + log.error(e.getMessage()); + throw new AssertionError("test fails"); + } + } + + protected void deleteListWithLogin(String uri, T request, Long userId) { + try { + String token = tokenProvider.createToken(userId); + + + mockMvc.perform(MockMvcRequestBuilders.delete(uri) .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) .header(AUTHORIZATION, BEARER + token)) .andExpect(MockMvcResultMatchers.status().isNoContent()) .andReturn(); @@ -248,8 +265,8 @@ protected void deleteWithLogin(String uri, Long resourceId, Long userId) { return postWithLogin(request, API_TAG_ATTACHMENT, TagAttachmentDTO.TagAttachmentResponse.class, userId); } - protected BoardPhotoDTO.BoardPhotoResponse 보드_사진_생성(Long userId, Long storagePhotoId, Long boardId) { - BoardPhotoDTO.AddBoardPhoto request = new BoardPhotoDTO.AddBoardPhoto(storagePhotoId, boardId); - return postWithLogin(request, API_BOARD_PHOTO, BoardPhotoDTO.BoardPhotoResponse.class, userId); + protected List 보드_사진_복수_생성(Long userId, List storagePhotoIds, Long boardId) { + BoardPhotoDTO.AddBoardPhoto request = new BoardPhotoDTO.AddBoardPhoto(storagePhotoIds, boardId); + return postListWithLogin(request, API_BOARD_PHOTO, BoardPhotoDTO.BoardPhotoResponse.class, userId); } } diff --git a/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java index fc5598d..4c6e9e0 100644 --- a/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java @@ -7,12 +7,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.stream.Collectors; + import static com.ddd.moodof.adapter.presentation.BoardPhotoController.API_BOARD_PHOTO; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; public class BoardPhotoAcceptanceTest extends AcceptanceTest { private StoragePhotoDTO.StoragePhotoResponse storagePhoto; + private StoragePhotoDTO.StoragePhotoResponse storagePhoto2; private BoardDTO.BoardResponse board; private CategoryDTO.CategoryResponse category; @@ -21,6 +25,7 @@ public class BoardPhotoAcceptanceTest extends AcceptanceTest { void setUp() { super.setUp(); storagePhoto = 보관함사진_생성(userId, "photoUri", "representativeColor"); + storagePhoto2 = 보관함사진_생성(userId, "photoUri2", "representativeColor2"); category = 카테고리_생성(userId, "title", 0L); board = 보드_생성(userId, 0L, category.getId(), "name"); } @@ -28,25 +33,28 @@ void setUp() { @Test void 보드에_보관함사진을_추가한다() { // when - BoardPhotoDTO.BoardPhotoResponse response = 보드_사진_생성(userId, storagePhoto.getId(), board.getId()); + List responses = 보드_사진_복수_생성(userId, List.of(storagePhoto.getId(), storagePhoto2.getId()), board.getId()); + List responses2 = 보드_사진_복수_생성(userId, List.of(storagePhoto.getId(), storagePhoto2.getId()), board.getId()); // then assertAll( - () -> assertThat(response.getId()).isNotNull(), - () -> assertThat(response.getUserId()).isEqualTo(userId), - () -> assertThat(response.getStoragePhotoId()).isEqualTo(storagePhoto.getId()), - () -> assertThat(response.getBoardId()).isEqualTo(board.getId()), - () -> assertThat(response.getCreatedDate()).isNotNull(), - () -> assertThat(response.getLastModifiedDate()).isEqualTo(response.getCreatedDate()) + () -> assertThat(responses.size()).isEqualTo(2), + () -> assertThat(responses.get(0).getPreviousBoardPhotoId()).isEqualTo(0L), + () -> assertThat(responses.get(1).getPreviousBoardPhotoId()).isEqualTo(responses.get(0).getId()), + () -> assertThat(responses2.get(0).getPreviousBoardPhotoId()).isEqualTo(responses.get(1).getId()), + () -> assertThat(responses2.get(1).getPreviousBoardPhotoId()).isEqualTo(responses2.get(0).getId()) ); } @Test void 보드에서_보관함사진을_제거한다() { // given - BoardPhotoDTO.BoardPhotoResponse response = 보드_사진_생성(userId, storagePhoto.getId(), board.getId()); + List responses = 보드_사진_복수_생성(userId, List.of(storagePhoto.getId()), board.getId()); // when then - deleteWithLogin(API_BOARD_PHOTO, response.getId(), userId); + List boardPhotoIds = responses.stream() + .map(BoardPhotoDTO.BoardPhotoResponse::getId) + .collect(Collectors.toList()); + deleteListWithLogin(API_BOARD_PHOTO, new BoardPhotoDTO.RemoveBoardPhotos(boardPhotoIds), userId); } } diff --git a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java index 39edac8..3562b78 100644 --- a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java @@ -152,12 +152,9 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { BoardDTO.BoardResponse board1 = 보드_생성(userId, 0L, category1.getId(), "board-1"); BoardDTO.BoardResponse board2 = 보드_생성(userId, board1.getId(), category1.getId(), "board-2"); BoardDTO.BoardResponse board3 = 보드_생성(userId, 0L, category2.getId(), "board-3"); - 보드_사진_생성(userId, storagePhoto1.getId(), board1.getId()); - 보드_사진_생성(userId, storagePhoto2.getId(), board1.getId()); - 보드_사진_생성(userId, storagePhoto2.getId(), board2.getId()); - 보드_사진_생성(userId, storagePhoto2.getId(), board3.getId()); - 보드_사진_생성(userId, storagePhoto3.getId(), board1.getId()); - 보드_사진_생성(userId, storagePhoto4.getId(), board2.getId()); + 보드_사진_복수_생성(userId, List.of(storagePhoto1.getId(), storagePhoto2.getId(), storagePhoto3.getId()), board1.getId()); + 보드_사진_복수_생성(userId, List.of(storagePhoto2.getId(), storagePhoto4.getId()), board2.getId()); + 보드_사진_복수_생성(userId, List.of(storagePhoto2.getId()), board3.getId()); TagDTO.TagResponse tag1 = 태그_생성(userId, "tag-1"); TagDTO.TagResponse tag2 = 태그_생성(userId, "tag-2"); 태그붙이기_생성(userId, storagePhoto2.getId(), tag1.getId()); From 28f23de242df52ead72a49f247015268ab142823 Mon Sep 17 00:00:00 2001 From: Gyeongjun Kim Date: Tue, 25 May 2021 22:37:46 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20BoardPhoto=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/presentation/BoardPhotoController.java | 7 +++++++ .../adapter/presentation/api/BoardPhotoAPI.java | 8 +++++--- .../ddd/moodof/application/BoardPhotoService.java | 5 +++++ .../model/board/photo/BoardPhotoRepository.java | 3 +++ .../moodof/acceptance/BoardPhotoAcceptanceTest.java | 12 ++++++++++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java b/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java index f337820..91c6c9b 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/BoardPhotoController.java @@ -25,6 +25,13 @@ public ResponseEntity> addPhotos(@LoginUs return ResponseEntity.ok(responses); } + @Override + @GetMapping + public ResponseEntity> findAllByBoard(@LoginUserId Long userId, @RequestParam Long boardId) { + List responses = boardPhotoService.findAllByBoardId(boardId, userId); + return ResponseEntity.ok(responses); + } + @Override @DeleteMapping public ResponseEntity removePhoto(@LoginUserId Long userId, @RequestBody BoardPhotoDTO.RemoveBoardPhotos request) { diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java index 0459b2c..3bab19c 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardPhotoAPI.java @@ -4,9 +4,7 @@ import com.ddd.moodof.application.dto.BoardPhotoDTO; import io.swagger.annotations.ApiImplicitParam; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.*; import springfox.documentation.annotations.ApiIgnore; import java.util.List; @@ -16,6 +14,10 @@ public interface BoardPhotoAPI { @PostMapping ResponseEntity> addPhotos(@ApiIgnore Long userId, @RequestBody BoardPhotoDTO.AddBoardPhoto request); + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") + @GetMapping + ResponseEntity> findAllByBoard(@ApiIgnore @LoginUserId Long userId, @RequestParam Long boardId); + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @DeleteMapping ResponseEntity removePhoto(@ApiIgnore @LoginUserId Long userId, @RequestBody BoardPhotoDTO.RemoveBoardPhotos request); diff --git a/src/main/java/com/ddd/moodof/application/BoardPhotoService.java b/src/main/java/com/ddd/moodof/application/BoardPhotoService.java index 71f8237..4a378d8 100644 --- a/src/main/java/com/ddd/moodof/application/BoardPhotoService.java +++ b/src/main/java/com/ddd/moodof/application/BoardPhotoService.java @@ -52,4 +52,9 @@ public void removePhoto(Long userId, BoardPhotoDTO.RemoveBoardPhotos request) { boardPhotoRepository.deleteAll(boardPhotos); } + + public List findAllByBoardId(Long boardId, Long userId) { + List boardPhotos = boardPhotoRepository.findAllByBoardIdAndUserId(boardId, userId); + return BoardPhotoDTO.BoardPhotoResponse.listFrom(boardPhotos); + } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java index 44136fd..81d4897 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/photo/BoardPhotoRepository.java @@ -2,8 +2,11 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface BoardPhotoRepository extends JpaRepository { Optional findFirstByBoardIdOrderByLastModifiedDateDesc(Long boardId); + + List findAllByBoardIdAndUserId(Long boardId, Long userId); } diff --git a/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java index 4c6e9e0..a16973b 100644 --- a/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/BoardPhotoAcceptanceTest.java @@ -46,6 +46,18 @@ void setUp() { ); } + @Test + void 보드사진을_조회한다() { + // given + 보드_사진_복수_생성(userId, List.of(storagePhoto.getId(), storagePhoto2.getId()), board.getId()); + + // when + List responses = getListWithLogin(API_BOARD_PHOTO + "?boardId=" + board.getId(), BoardPhotoDTO.BoardPhotoResponse.class, userId); + + // then + assertThat(responses.size()).isEqualTo(2); + } + @Test void 보드에서_보관함사진을_제거한다() { // given From 55d3be7ae99d427ba0c65875024c4790eec206af Mon Sep 17 00:00:00 2001 From: Gyeongjun Kim Date: Tue, 25 May 2021 22:45:47 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20storagePhoto=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=82=AC=EC=A7=84=20=EA=B0=9C=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../querydsl/StoragePhotoQuerydslRepository.java | 4 +++- .../com/ddd/moodof/application/dto/StoragePhotoDTO.java | 1 + .../ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java index 7956fb1..56a40ce 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java @@ -50,7 +50,9 @@ public StoragePhotoDTO.StoragePhotoPageResponse findPageExcludeTrash(Long userId .applyPagination(pageable, jpaQuery) .fetch(); - return new StoragePhotoDTO.StoragePhotoPageResponse(paginationUtils.getTotalPageCount(jpaQuery.fetchCount(), pageable.getPageSize()), responses); + long totalCount = jpaQuery.fetchCount(); + + return new StoragePhotoDTO.StoragePhotoPageResponse(totalCount, paginationUtils.getTotalPageCount(totalCount, pageable.getPageSize()), responses); } private JPAQuery getQuery(Long userId, List tagIds) { diff --git a/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java b/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java index f30a16f..405a219 100644 --- a/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/StoragePhotoDTO.java @@ -53,6 +53,7 @@ public static List listFrom(List storagePhot @Getter @AllArgsConstructor public static class StoragePhotoPageResponse { + private long totalStoragePhotoCount; private long totalPageCount; private List storagePhotos; } diff --git a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java index 3562b78..7dd4c4d 100644 --- a/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/StoragePhotoAcceptanceTest.java @@ -87,7 +87,8 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { () -> assertThat(response.getStoragePhotos().size()).isEqualTo(3), () -> assertThat(response.getStoragePhotos().get(0)).usingRecursiveComparison().isEqualTo(top), () -> assertThat(response.getStoragePhotos().get(1)).usingRecursiveComparison().isEqualTo(second), - () -> assertThat(response.getTotalPageCount()).isEqualTo(2) + () -> assertThat(response.getTotalPageCount()).isEqualTo(2), + () -> assertThat(response.getTotalStoragePhotoCount()).isEqualTo(4) ); } @@ -126,7 +127,8 @@ public class StoragePhotoAcceptanceTest extends AcceptanceTest { () -> assertThat(response.getStoragePhotos().size()).isEqualTo(2), () -> assertThat(response.getStoragePhotos().get(0)).usingRecursiveComparison().isEqualTo(top), () -> assertThat(response.getStoragePhotos().get(1)).usingRecursiveComparison().isEqualTo(second), - () -> assertThat(response.getTotalPageCount()).isEqualTo(2) + () -> assertThat(response.getTotalPageCount()).isEqualTo(2), + () -> assertThat(response.getTotalStoragePhotoCount()).isEqualTo(4) ); } From f2862a642b0f85c44a2ddd99e62e0a87030a5845 Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Thu, 27 May 2021 21:00:15 +0900 Subject: [PATCH 07/12] fix: code conflict --- .gitignore | 3 +- .../configuration/EncryptConfig.java | 14 +++ .../configuration/WebMvcConfig.java | 4 + .../SharedBoardIdArgumentResolver.java | 46 ++++++++++ .../security/encrypt/EncryptUtil.java | 57 +++++++++++++ .../presentation/PublicController.java | 40 +++++++++ .../adapter/presentation/SharedBoardId.java | 10 +++ .../presentation/SharedController.java | 27 ++++++ .../adapter/presentation/api/PublicAPI.java | 15 ++++ .../adapter/presentation/api/SharedAPI.java | 17 ++++ .../ddd/moodof/application/BoardService.java | 85 +++++++++++++++++++ .../moodof/application/CategoryService.java | 1 - .../ddd/moodof/application/dto/BoardDTO.java | 9 +- .../ddd/moodof/application/dto/SharedDTO.java | 31 +++++++ .../application/verifier/BoardVerifier.java | 2 +- .../ddd/moodof/domain/model/board/Board.java | 8 ++ .../domain/model/board/BoardRepository.java | 2 + .../model/board/BoardSharedKeyUpdater.java | 14 +++ src/main/resources/application.yml | 2 + .../ddd/moodof/acceptance/AcceptanceTest.java | 36 ++++++++ .../acceptance/SharedAcceptanceTest.java | 42 +++++++++ 21 files changed, 460 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java create mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/SharedBoardId.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java create mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java create mode 100644 src/main/java/com/ddd/moodof/application/dto/SharedDTO.java create mode 100644 src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java create mode 100644 src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java diff --git a/.gitignore b/.gitignore index 4bf68a5..d9d3c22 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ ### VS Code ### .vscode/ -application-security.yml \ No newline at end of file +application-security.yml +/src/main/resources/application-encrypto.yml diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java new file mode 100644 index 0000000..885052b --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java @@ -0,0 +1,14 @@ +package com.ddd.moodof.adapter.infrastructure.configuration; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "encrypt") +@Getter +@Setter +public class EncryptConfig { + private String key; +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java index 69ef7ac..5a2ef48 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java @@ -1,6 +1,7 @@ package com.ddd.moodof.adapter.infrastructure.configuration; import com.ddd.moodof.adapter.infrastructure.security.LoginUserIdArgumentResolver; +import com.ddd.moodof.adapter.infrastructure.security.SharedBoardIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -16,9 +17,12 @@ public class WebMvcConfig implements WebMvcConfigurer { private final LoginUserIdArgumentResolver loginUserIdArgumentResolver; + private final SharedBoardIdArgumentResolver sharedBoardIdArgumentResolver; + @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginUserIdArgumentResolver); + resolvers.add(sharedBoardIdArgumentResolver); } @Override diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java new file mode 100644 index 0000000..9456d3a --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java @@ -0,0 +1,46 @@ +package com.ddd.moodof.adapter.infrastructure.security; + +import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; + +import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; +import com.ddd.moodof.adapter.presentation.SharedBoardId; +import com.ddd.moodof.domain.model.board.BoardRepository; +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; + +@RequiredArgsConstructor +@Component +public class SharedBoardIdArgumentResolver implements HandlerMethodArgumentResolver { + + private final BoardRepository boardRepository; + + private final EncryptConfig encryptConfig; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(SharedBoardId.class); + } + + @Override + public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + SharedBoardId customParam = parameter.getParameterAnnotation(SharedBoardId.class); + String key = webRequest.getParameter(customParam.value()); + + try { + String id = EncryptUtil.decryptAES256(key, encryptConfig.getKey()); + Long boardId = Long.valueOf(id); + boardRepository.findById(boardId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id = " + id)); + return boardId; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java new file mode 100644 index 0000000..5b0879f --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java @@ -0,0 +1,57 @@ +package com.ddd.moodof.adapter.infrastructure.security.encrypt; + +import org.springframework.stereotype.Component; +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.SecureRandom; +import java.util.Base64; + +@Component +public class EncryptUtil { + + public static String encryptAES256(String msg, String key) throws Exception { + SecureRandom random = new SecureRandom(); + byte bytes[] = new byte[20]; + random.nextBytes(bytes); + byte[] saltBytes = bytes; + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256); + SecretKey secretKey = factory.generateSecret(spec); + SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secret); + AlgorithmParameters params = cipher.getParameters(); + + byte[] ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV(); + byte[] encryptedTextBytes = cipher.doFinal(msg.getBytes("UTF-8")); + byte[] buffer = new byte[saltBytes.length + ivBytes.length + encryptedTextBytes.length]; + System.arraycopy(saltBytes, 0, buffer, 0, saltBytes.length); + System.arraycopy(ivBytes, 0, buffer, saltBytes.length, ivBytes.length); + System.arraycopy(encryptedTextBytes, 0, buffer, saltBytes.length + ivBytes.length, encryptedTextBytes.length); + return Base64.getEncoder().encodeToString(buffer); + } + public static String decryptAES256(String msg, String key) throws Exception { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + ByteBuffer buffer = ByteBuffer.wrap(Base64.getDecoder().decode(msg)); + byte[] saltBytes = new byte[20]; + byte[] ivBytes = new byte[cipher.getBlockSize()]; + byte[] encryoptedTextBytes = new byte[buffer.capacity() - saltBytes.length - ivBytes.length]; + + buffer.get(saltBytes, 0, saltBytes.length); + buffer.get(ivBytes, 0, ivBytes.length); + buffer.get(encryoptedTextBytes); + + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256); + SecretKey secretKey = factory.generateSecret(spec); + SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes)); + byte[] decryptedTextBytes = cipher.doFinal(encryoptedTextBytes); + return new String(decryptedTextBytes); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java b/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java new file mode 100644 index 0000000..dc4b7e3 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java @@ -0,0 +1,40 @@ +package com.ddd.moodof.adapter.presentation; + + +import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; +import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; +import com.ddd.moodof.adapter.presentation.api.PublicAPI; +import com.ddd.moodof.application.BoardService; +import com.ddd.moodof.application.CategoryService; +import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.CategoryDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +// todo 존재하는 id값인지 확인하기 +// todo public 경로로 direct를 진행한다. +// todo public 경로의 cors mapping을 풀어준다. +@RequiredArgsConstructor +@RequestMapping(PublicController.API_PUBLIC) +@RestController +public class PublicController implements PublicAPI { + public static final String API_PUBLIC = "/api/public"; + public static final String BOARDS = "/boards/{id}"; + private final EncryptConfig encryptConfig; + private final BoardService boardService; + private final CategoryService categoryService; + + @GetMapping(BOARDS) + public ResponseEntity> getSharedBoard(@PathVariable String sharedKey){ + List responses = null; +// = boardService.findBySharedKey(sharedKey); + return ResponseEntity.ok(responses); + } +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/SharedBoardId.java b/src/main/java/com/ddd/moodof/adapter/presentation/SharedBoardId.java new file mode 100644 index 0000000..24f6765 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/SharedBoardId.java @@ -0,0 +1,10 @@ +package com.ddd.moodof.adapter.presentation; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SharedBoardId { + String value() default ""; +} \ No newline at end of file diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java b/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java new file mode 100644 index 0000000..3708748 --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java @@ -0,0 +1,27 @@ +package com.ddd.moodof.adapter.presentation; + +import com.ddd.moodof.adapter.presentation.api.SharedAPI; +import com.ddd.moodof.application.BoardService; +import com.ddd.moodof.application.dto.SharedDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; +import java.net.URI; + +@RequiredArgsConstructor +@RequestMapping(SharedController.API_SHARED) +@RestController +public class SharedController implements SharedAPI { + public static final String API_SHARED = "/api/shared"; + public static final String BOARDS = "/boards"; + + private final BoardService boardService; + + @PostMapping(BOARDS) + public ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest){ + SharedDTO.SharedBoardResponse response = boardService.createSharedKey(request.getId(), userId, httpServletRequest); + return ResponseEntity.created(URI.create(API_SHARED + BOARDS + "/" + response.getId())).body(response); + } + +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java new file mode 100644 index 0000000..4d55cda --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java @@ -0,0 +1,15 @@ +package com.ddd.moodof.adapter.presentation.api; + +import com.ddd.moodof.adapter.presentation.PublicController; +import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.CategoryDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +public interface PublicAPI { + @GetMapping(PublicController.BOARDS) + ResponseEntity> getSharedBoard(@PathVariable String sharedKey); +} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java new file mode 100644 index 0000000..b86f88b --- /dev/null +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java @@ -0,0 +1,17 @@ +package com.ddd.moodof.adapter.presentation.api; + +import com.ddd.moodof.adapter.presentation.LoginUserId; +import com.ddd.moodof.application.dto.SharedDTO; +import io.swagger.annotations.ApiImplicitParam; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.servlet.http.HttpServletRequest; + +public interface SharedAPI { + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") + @PostMapping + ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); + +} diff --git a/src/main/java/com/ddd/moodof/application/BoardService.java b/src/main/java/com/ddd/moodof/application/BoardService.java index 3d582f2..b4e690c 100644 --- a/src/main/java/com/ddd/moodof/application/BoardService.java +++ b/src/main/java/com/ddd/moodof/application/BoardService.java @@ -1,22 +1,43 @@ package com.ddd.moodof.application; +import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; +import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.dto.SharedDTO; import com.ddd.moodof.application.verifier.BoardVerifier; import com.ddd.moodof.domain.model.board.Board; +import com.ddd.moodof.domain.model.board.BoardSharedKeyUpdater; import com.ddd.moodof.domain.model.board.BoardRepository; import com.ddd.moodof.domain.model.board.BoardSequenceUpdater; +import com.ddd.moodof.domain.model.category.Category; import lombok.RequiredArgsConstructor; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import javax.servlet.http.HttpServletRequest; import javax.transaction.Transactional; +import java.security.GeneralSecurityException; +import java.util.List; @RequiredArgsConstructor @Service public class BoardService { + private final BoardRepository boardRepository; + private final BoardVerifier boardVerifier; + private final BoardSequenceUpdater boardSequenceUpdater; + private final BoardSharedKeyUpdater boardSharedKeyUpdater; + + private final EncryptConfig encryptConfig; + + public static final String LOCALHOST = "localhost"; + @Transactional public BoardDTO.BoardResponse create(Long userId, BoardDTO.CreateBoard request) { Board board = boardVerifier.toEntity(request.getPreviousBoardId(), request.getCategoryId(), request.getName(), userId); @@ -52,4 +73,68 @@ public void delete(Long userId, Long id) { } boardRepository.deleteById(id); } + + public SharedDTO.SharedBoardResponse createSharedKey(Long id, Long userId, HttpServletRequest httpServletRequest) { + //todo 암호화된 값을 board로 등록시킨다 + String requestURL= getUrl(httpServletRequest); + String sharedKey = null; + try { + sharedKey = EncryptUtil.encryptAES256(String.valueOf(id), encryptConfig.getKey()); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + Board board = boardRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Board, id = " + id)); + boardSharedKeyUpdater.update(board, sharedKey, userId); + return createBoardsURL(id, requestURL, sharedKey); + } + + public SharedDTO.SharedBoardResponse createBoardsURL(Long id, String requestURL, String sharedKey) { + return generatedURL(id, requestURL, sharedKey); + } +// public List findBySharedKey(String sharedKey) { +// Board board = boardRepository.findBySharedKey(sharedKey) +// .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey)); +//// List totalCategories = categoryRepository.findAllByUserId(board.getUserId()); +//// return CategoryDTO.CategoryResponse.listForm(totalCategories); +// } + + + private SharedDTO.SharedBoardResponse generatedURL(Long id, String requestURL, String sharedKey){ + StringBuilder sharedURL = new StringBuilder(); + sharedURL.append(requestURL) + .append("/") + .append(sharedKey); + return SharedDTO.SharedBoardResponse.from(id,sharedURL.toString(), sharedKey); + } + + + + public String getUrl(HttpServletRequest request) { + ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request); + UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); + + String scheme = uriComponents.getScheme(); // http / https + String serverName = request.getServerName(); // hostname.com + int serverPort = request.getServerPort(); // 80 + String contextPath = request.getContextPath(); // /app + + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://"); + url.append(serverName); + + if (serverName.equals(LOCALHOST)) { + url.append(":").append(serverPort); + }else { + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + } + url.append(contextPath); + return url.toString(); + } } diff --git a/src/main/java/com/ddd/moodof/application/CategoryService.java b/src/main/java/com/ddd/moodof/application/CategoryService.java index cc29423..148f4ae 100644 --- a/src/main/java/com/ddd/moodof/application/CategoryService.java +++ b/src/main/java/com/ddd/moodof/application/CategoryService.java @@ -9,7 +9,6 @@ import org.springframework.stereotype.Service; import javax.transaction.Transactional; -import java.util.List; import java.util.Optional; @RequiredArgsConstructor diff --git a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java index ed4e2b0..494e1db 100644 --- a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java @@ -18,8 +18,12 @@ public static class CreateBoard { private String name; public Board toEntity(Long userId) { - return new Board(null, previousBoardId, userId, name, categoryId, null, null); + return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "", null, null)); } + public Board setSharedKey(Board board){ + return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); + } + } @NoArgsConstructor @@ -31,11 +35,12 @@ public static class BoardResponse { private Long userId; private String name; private Long categoryId; + private String sharedKey; private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; public static BoardResponse from(Board board) { - return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getCreatedDate(), board.getLastModifiedDate()); + return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); } } diff --git a/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java b/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java new file mode 100644 index 0000000..f3b7bf8 --- /dev/null +++ b/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java @@ -0,0 +1,31 @@ +package com.ddd.moodof.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +public class SharedDTO { + @NoArgsConstructor + @AllArgsConstructor + @Getter + public static class SharedBoardRequest{ + private Long id; + } + + @NoArgsConstructor + @Getter + @AllArgsConstructor + public static class SharedBoardResponse { + private Long id; + + private String sharedURL; + + private String sharedKey; + + public static SharedDTO.SharedBoardResponse from(Long id, String sharedURL, String sharedKey) { + return new SharedDTO.SharedBoardResponse(id, sharedURL, sharedKey); + } + } +} diff --git a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java index 60e5cc3..d34db48 100644 --- a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java +++ b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java @@ -19,6 +19,6 @@ public Board toEntity(Long previousBoardId, Long categoryId, String name, Long u throw new IllegalArgumentException("카테고리 생성자와 로그인 유저의 아이디가 다릅니다."); } - return new Board(null, previousBoardId, userId, name, categoryId, null, null); + return new Board(null, previousBoardId, userId, name, categoryId, null, null, null); } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/Board.java b/src/main/java/com/ddd/moodof/domain/model/board/Board.java index 95404dc..ec46792 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/Board.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/Board.java @@ -29,6 +29,8 @@ public class Board { private Long categoryId; + private String sharedKey; + @CreatedDate private LocalDateTime createdDate; @@ -54,6 +56,12 @@ public void updateSequence(Long previousBoardId, Long categoryId, Long userId) { this.categoryId = categoryId; } + public void updateSharedkey(String sharedKey, Long userId){ + verify(userId); + this.sharedKey = sharedKey; + } + + private void verify(Long userId) { if (isUserNotEqual(userId)) { throw new IllegalArgumentException("userId가 일치하지 않습니다."); diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java index fbb45ec..473a724 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java @@ -11,6 +11,8 @@ public interface BoardRepository extends JpaRepository { Optional findByPreviousBoardIdAndCategoryId(Long previousBoardId, Long categoryId); + Optional findBySharedKey(String sharedKey); + void deleteAllByCategoryId(Long categoryId); boolean existsByIdAndUserId(Long id, Long userId); diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java new file mode 100644 index 0000000..c18a9d0 --- /dev/null +++ b/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java @@ -0,0 +1,14 @@ +package com.ddd.moodof.domain.model.board; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class BoardSharedKeyUpdater { + private final BoardRepository boardRepository; + public Board update(Board board, String sharedKey, Long userId){ + board.updateSharedkey(sharedKey, userId); + return boardRepository.save(board); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bd88395..04ae6a6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,3 +21,5 @@ spring: logging.level: org.hibernate.SQL: debug +server: + port: 80 \ No newline at end of file diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index e1f369a..9974674 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -1,6 +1,7 @@ package com.ddd.moodof.acceptance; import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; +import com.ddd.moodof.adapter.presentation.SharedController; import com.ddd.moodof.application.dto.*; import com.ddd.moodof.domain.model.user.AuthProvider; import com.ddd.moodof.domain.model.user.User; @@ -40,6 +41,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class AcceptanceTest { private static final String BEARER = "Bearer "; + private static final String BOARDS = "/boards"; protected Long userId; @@ -73,6 +75,12 @@ protected User signUp() { return userRepository.save(new User(null, "test@test.com", "password", "nickname", "profileUrl", null, null, AuthProvider.google, "providerId")); } + protected SharedDTO.SharedBoardResponse 보드_공유하기(Long id, Long userId){ + SharedDTO.SharedBoardRequest request = new SharedDTO.SharedBoardRequest(id); + + return postWithLogin(request, SharedController.API_SHARED + BOARDS, SharedDTO.SharedBoardResponse.class, userId); + } + protected CategoryDTO.CategoryResponse 카테고리_생성(Long userId, String title, Long previousId){ CategoryDTO.CreateCategoryRequest request = new CategoryDTO.CreateCategoryRequest(title,previousId); return postWithLogin(request, API_CATEGORY, CategoryDTO.CategoryResponse.class, userId); @@ -217,6 +225,34 @@ protected List getListWithLogin(String uri, Class response, Long userI .header(AUTHORIZATION, BEARER + token)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); + CollectionType collectionType = objectMapper.getTypeFactory().constructCollectionType(List.class, response); + + return objectMapper.readValue(result.getResponse().getContentAsString(), collectionType); + } catch (Exception e) { + log.error(e.getMessage()); + throw new AssertionError("test fails"); + } + } + protected T getPublicWithLogin(String uri, Class response) { + try { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + return objectMapper.readValue(result.getResponse().getContentAsString(), response); + } catch (Exception e) { + log.error(e.getMessage()); + throw new AssertionError("test fails"); + } + } + + protected List getPublicListWithLogin(String uri, Class response, String resourceId) { + try { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri ,resourceId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); CollectionType collectionType = objectMapper.getTypeFactory().constructCollectionType(List.class, response); diff --git a/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java new file mode 100644 index 0000000..fee68ca --- /dev/null +++ b/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java @@ -0,0 +1,42 @@ +package com.ddd.moodof.acceptance; + +import com.ddd.moodof.adapter.presentation.PublicController; +import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.dto.SharedDTO; +import org.junit.jupiter.api.Test; + +import java.util.List; + + +public class SharedAcceptanceTest extends AcceptanceTest{ + @Test + public void 공유_URL_생성() throws Exception { + // given + long previousBoardId = 0L; + String name = "name"; + CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); + BoardDTO.BoardResponse response = 보드_생성(userId, previousBoardId, category.getId(), name); + + // when + SharedDTO.SharedBoardResponse response1 = 보드_공유하기(response.getId(), userId); + + // then + + } + @Test + public void 권한없이_보드_조회() throws Exception { + // given + long previousBoardId = 0L; + String name = "name"; + CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); + BoardDTO.BoardResponse board = 보드_생성(userId, previousBoardId, category.getId(), name); + SharedDTO.SharedBoardResponse response = 보드_공유하기(board.getId(), userId); + + List publicListWithLogin = getPublicListWithLogin(PublicController.API_PUBLIC + PublicController.BOARDS, BoardDTO.BoardResponse.class, response.getSharedKey()); + + // when + + // then + } +} From 41344c8db91ddca375ef98dd8ce98daa5ca7446a Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Fri, 28 May 2021 15:05:36 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20URL=EC=83=9D=EC=84=B1,=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC,=20?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/MockDataLoader.java | 164 +++++++++--------- .../querydsl/CategoryQuerydslRepository.java | 2 + .../adapter/presentation/BoardController.java | 8 + .../presentation/PublicController.java | 40 ++--- .../presentation/SharedController.java | 27 --- .../adapter/presentation/api/BoardAPI.java | 7 + .../adapter/presentation/api/PublicAPI.java | 15 +- .../adapter/presentation/api/SharedAPI.java | 12 +- .../ddd/moodof/application/BoardService.java | 76 ++++---- .../moodof/application/CategoryService.java | 1 + .../application/StoragePhotoService.java | 15 ++ .../ddd/moodof/application/dto/BoardDTO.java | 31 +++- .../ddd/moodof/application/dto/SharedDTO.java | 20 --- .../application/verifier/BoardVerifier.java | 2 +- .../ddd/moodof/domain/model/board/Board.java | 6 +- .../domain/model/board/BoardRepository.java | 1 + .../model/board/BoardSharedKeyUpdater.java | 4 +- src/main/resources/application.yml | 4 +- .../ddd/moodof/acceptance/AcceptanceTest.java | 25 ++- .../acceptance/BoardAcceptanceTest.java | 4 +- .../acceptance/BoardSharedAcceptanceTest.java | 143 +++++++++++++++ .../acceptance/SharedAcceptanceTest.java | 42 ----- 22 files changed, 376 insertions(+), 273 deletions(-) delete mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java create mode 100644 src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java delete mode 100644 src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/MockDataLoader.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/MockDataLoader.java index fed3ba4..00bc897 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/MockDataLoader.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/MockDataLoader.java @@ -1,82 +1,82 @@ -package com.ddd.moodof.adapter.infrastructure.persistence; - -import com.ddd.moodof.application.*; -import com.ddd.moodof.application.dto.*; -import com.ddd.moodof.domain.model.user.AuthProvider; -import com.ddd.moodof.domain.model.user.User; -import com.ddd.moodof.domain.model.user.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.stream.IntStream; - - -@RequiredArgsConstructor -@Profile("dev & !test") -@Component -public class MockDataLoader implements ApplicationRunner { - private final UserRepository userRepository; - private final BoardService boardService; - private final CategoryService categoryService; - private final StoragePhotoService storagePhotoService; - private final TagAttachmentService tagAttachmentService; - private final TagService tagService; - private final TrashPhotoService trashPhotoService; - - @Value("${dev.user.email}") - private String email; - @Value("${dev.user.nickname}") - private String nickname; - @Value("${dev.user.profileUrl}") - private String profileUrl; - @Value("${dev.user.providerId}") - private String providerId; - - @Override - public void run(ApplicationArguments args) { - User user = userRepository.save(new User(null, email, null, nickname, profileUrl, null, null, AuthProvider.google, providerId)); - Long userId = user.getId(); - IntStream.range(0, 10) - .forEach(__ -> createPhotos(userId)); - - StoragePhotoDTO.StoragePhotoResponse 사진1 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://pbs.twimg.com/media/ExUElF7VcAMx7jx.jpg", "representativeColor"), userId); - StoragePhotoDTO.StoragePhotoResponse 사진2 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://spnimage.edaily.co.kr/images/Photo/files/NP/S/2020/10/PS20100800026.jpg", "representativeColor"), userId); - StoragePhotoDTO.StoragePhotoResponse 사진3 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://file2.nocutnews.co.kr/newsroom/image/2021/03/25/202103251659468108_0.jpg", "representativeColor"), userId); - StoragePhotoDTO.StoragePhotoResponse 사진4 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://image.kmib.co.kr/online_image/2019/1126/611816110013967774_1.jpg", "representativeColor"), userId); - StoragePhotoDTO.StoragePhotoResponse 사진5 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://www.jejuilbo.net/news/photo/202102/157830_101125_2752.jpg", "representativeColor"), userId); - CategoryDTO.CategoryResponse 카테고리1 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 1", 0L), userId); - CategoryDTO.CategoryResponse 카테고리2 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 2", 카테고리1.getId()), userId); - CategoryDTO.CategoryResponse 카테고리3 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 3", 카테고리2.getId()), userId); - BoardDTO.BoardResponse 보드1 = boardService.create(userId, new BoardDTO.CreateBoard(0L, 카테고리1.getId(), "보드 1")); - BoardDTO.BoardResponse 보드2 = boardService.create(userId, new BoardDTO.CreateBoard(보드1.getId(), 카테고리1.getId(), "보드 2")); - BoardDTO.BoardResponse 보드3 = boardService.create(userId, new BoardDTO.CreateBoard(보드2.getId(), 카테고리1.getId(), "보드 3")); - BoardDTO.BoardResponse 보드4 = boardService.create(userId, new BoardDTO.CreateBoard(0L, 카테고리2.getId(), "보드 4")); - BoardDTO.BoardResponse 보드5 = boardService.create(userId, new BoardDTO.CreateBoard(보드4.getId(), 카테고리2.getId(), "보드 5")); - TagDTO.TagResponse 태그1 = tagService.create(new TagDTO.CreateRequest("태그 1"), userId); - TagDTO.TagResponse 태그2 = tagService.create(new TagDTO.CreateRequest("태그 2"), userId); - TagDTO.TagResponse 태그3 = tagService.create(new TagDTO.CreateRequest("태그 3"), userId); - TagDTO.TagResponse 태그4 = tagService.create(new TagDTO.CreateRequest("태그 4"), userId); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그1.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그2.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그3.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진2.getId(), 태그1.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진2.getId(), 태그2.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진3.getId(), 태그3.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진3.getId(), 태그1.getId())); - tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진4.getId(), 태그4.getId())); - trashPhotoService.add(userId, new TrashPhotoDTO.CreateTrashPhotos(List.of(사진4.getId(), 사진5.getId()))); - } - - private void createPhotos(Long userId) { - storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://cdn.mhnse.com/news/photo/202103/71190_41523_2339.jpg", "representativeColor"), userId); - storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://dimg.donga.com/wps/NEWS/IMAGE/2021/01/28/105164933.2.jpg", "representativeColor"), userId); - storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://img.vogue.co.kr/vogue/2021/04/style_607fb99974fc2-751x930.jpg", "representativeColor"), userId); - storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://image.ytn.co.kr/general/jpg/2020/0723/202007231130167111_d.jpg", "representativeColor"), userId); - storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://www.enewstoday.co.kr/news/photo/201909/1336361_400469_529.jpg", "representativeColor"), userId); - } -} +//package com.ddd.moodof.adapter.infrastructure.persistence; +// +//import com.ddd.moodof.application.*; +//import com.ddd.moodof.application.dto.*; +//import com.ddd.moodof.domain.model.user.AuthProvider; +//import com.ddd.moodof.domain.model.user.User; +//import com.ddd.moodof.domain.model.user.UserRepository; +//import lombok.RequiredArgsConstructor; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.boot.ApplicationArguments; +//import org.springframework.boot.ApplicationRunner; +//import org.springframework.context.annotation.Profile; +//import org.springframework.stereotype.Component; +// +//import java.util.List; +//import java.util.stream.IntStream; +// +// +//@RequiredArgsConstructor +//@Profile("dev & !test") +//@Component +//public class MockDataLoader implements ApplicationRunner { +// private final UserRepository userRepository; +// private final BoardService boardService; +// private final CategoryService categoryService; +// private final StoragePhotoService storagePhotoService; +// private final TagAttachmentService tagAttachmentService; +// private final TagService tagService; +// private final TrashPhotoService trashPhotoService; +// +// @Value("${dev.user.email}") +// private String email; +// @Value("${dev.user.nickname}") +// private String nickname; +// @Value("${dev.user.profileUrl}") +// private String profileUrl; +// @Value("${dev.user.providerId}") +// private String providerId; +// +// @Override +// public void run(ApplicationArguments args) { +// User user = userRepository.save(new User(null, email, null, nickname, profileUrl, null, null, AuthProvider.google, providerId)); +// Long userId = user.getId(); +// IntStream.range(0, 10) +// .forEach(__ -> createPhotos(userId)); +// +// StoragePhotoDTO.StoragePhotoResponse 사진1 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://pbs.twimg.com/media/ExUElF7VcAMx7jx.jpg", "representativeColor"), userId); +// StoragePhotoDTO.StoragePhotoResponse 사진2 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://spnimage.edaily.co.kr/images/Photo/files/NP/S/2020/10/PS20100800026.jpg", "representativeColor"), userId); +// StoragePhotoDTO.StoragePhotoResponse 사진3 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://file2.nocutnews.co.kr/newsroom/image/2021/03/25/202103251659468108_0.jpg", "representativeColor"), userId); +// StoragePhotoDTO.StoragePhotoResponse 사진4 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://image.kmib.co.kr/online_image/2019/1126/611816110013967774_1.jpg", "representativeColor"), userId); +// StoragePhotoDTO.StoragePhotoResponse 사진5 = storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://www.jejuilbo.net/news/photo/202102/157830_101125_2752.jpg", "representativeColor"), userId); +// CategoryDTO.CategoryResponse 카테고리1 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 1", 0L), userId); +// CategoryDTO.CategoryResponse 카테고리2 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 2", 카테고리1.getId()), userId); +// CategoryDTO.CategoryResponse 카테고리3 = categoryService.create(new CategoryDTO.CreateCategoryRequest("카테고리 3", 카테고리2.getId()), userId); +// BoardDTO.BoardResponse 보드1 = boardService.create(userId, new BoardDTO.CreateBoard(0L, 카테고리1.getId(), "보드 1")); +// BoardDTO.BoardResponse 보드2 = boardService.create(userId, new BoardDTO.CreateBoard(보드1.getId(), 카테고리1.getId(), "보드 2")); +// BoardDTO.BoardResponse 보드3 = boardService.create(userId, new BoardDTO.CreateBoard(보드2.getId(), 카테고리1.getId(), "보드 3")); +// BoardDTO.BoardResponse 보드4 = boardService.create(userId, new BoardDTO.CreateBoard(0L, 카테고리2.getId(), "보드 4")); +// BoardDTO.BoardResponse 보드5 = boardService.create(userId, new BoardDTO.CreateBoard(보드4.getId(), 카테고리2.getId(), "보드 5")); +// TagDTO.TagResponse 태그1 = tagService.create(new TagDTO.CreateRequest("태그 1"), userId); +// TagDTO.TagResponse 태그2 = tagService.create(new TagDTO.CreateRequest("태그 2"), userId); +// TagDTO.TagResponse 태그3 = tagService.create(new TagDTO.CreateRequest("태그 3"), userId); +// TagDTO.TagResponse 태그4 = tagService.create(new TagDTO.CreateRequest("태그 4"), userId); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그1.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그2.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진1.getId(), 태그3.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진2.getId(), 태그1.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진2.getId(), 태그2.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진3.getId(), 태그3.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진3.getId(), 태그1.getId())); +// tagAttachmentService.create(userId, new TagAttachmentDTO.CreateTagAttachment(사진4.getId(), 태그4.getId())); +// trashPhotoService.add(userId, new TrashPhotoDTO.CreateTrashPhotos(List.of(사진4.getId(), 사진5.getId()))); +// } +// +// private void createPhotos(Long userId) { +// storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://cdn.mhnse.com/news/photo/202103/71190_41523_2339.jpg", "representativeColor"), userId); +// storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://dimg.donga.com/wps/NEWS/IMAGE/2021/01/28/105164933.2.jpg", "representativeColor"), userId); +// storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("http://img.vogue.co.kr/vogue/2021/04/style_607fb99974fc2-751x930.jpg", "representativeColor"), userId); +// storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://image.ytn.co.kr/general/jpg/2020/0723/202007231130167111_d.jpg", "representativeColor"), userId); +// storagePhotoService.create(new StoragePhotoDTO.CreateStoragePhoto("https://www.enewstoday.co.kr/news/photo/201909/1336361_400469_529.jpg", "representativeColor"), userId); +// } +//} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/CategoryQuerydslRepository.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/CategoryQuerydslRepository.java index 0fd04e1..2ecea1a 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/CategoryQuerydslRepository.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/CategoryQuerydslRepository.java @@ -34,6 +34,7 @@ public List findAllByUserId(Long userId) category.lastModifiedDate)) .from(category) .where(includeUserId) + .orderBy(category.id.asc()) .fetch(); for (CategoryDTO.CategoryResponse category : categories) { @@ -48,6 +49,7 @@ public List findAllByUserId(Long userId) board.lastModifiedDate)) .from(board) .where(board.userId.eq(category.getUserId()).and(board.categoryId.eq(category.getId()))) + .orderBy(board.id.asc()) .fetch(); categoryWithBoardList.add(CategoryDTO.CategoryWithBoardResponse.from(category, boards)); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java b/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java index 57b420e..caa58ed 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java @@ -3,10 +3,12 @@ import com.ddd.moodof.adapter.presentation.api.BoardAPI; import com.ddd.moodof.application.BoardService; import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.SharedDTO; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; import java.net.URI; @RequiredArgsConstructor @@ -44,4 +46,10 @@ public ResponseEntity delete(@LoginUserId Long userId, @ boardService.delete(userId, id); return ResponseEntity.noContent().build(); } + + @PostMapping("/shared") + public ResponseEntity create(@RequestBody BoardDTO.BoardSharedRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest){ + BoardDTO.BoardSharedResponse response = boardService.createSharedKey(request.getId(), userId, httpServletRequest); + return ResponseEntity.created(URI.create(API_BOARD + "/shared/" + response.getId())).body(response); + } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java b/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java index dc4b7e3..d7c17fc 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java @@ -1,40 +1,36 @@ package com.ddd.moodof.adapter.presentation; - -import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; -import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; import com.ddd.moodof.adapter.presentation.api.PublicAPI; import com.ddd.moodof.application.BoardService; -import com.ddd.moodof.application.CategoryService; -import com.ddd.moodof.application.dto.BoardDTO; -import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.StoragePhotoService; +import com.ddd.moodof.application.dto.StoragePhotoDTO; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.List; -// todo 존재하는 id값인지 확인하기 -// todo public 경로로 direct를 진행한다. -// todo public 경로의 cors mapping을 풀어준다. @RequiredArgsConstructor @RequestMapping(PublicController.API_PUBLIC) @RestController public class PublicController implements PublicAPI { + public static final String API_PUBLIC = "/api/public"; - public static final String BOARDS = "/boards/{id}"; - private final EncryptConfig encryptConfig; + + private final StoragePhotoService storagePhotoService; + private final BoardService boardService; - private final CategoryService categoryService; - @GetMapping(BOARDS) - public ResponseEntity> getSharedBoard(@PathVariable String sharedKey){ - List responses = null; -// = boardService.findBySharedKey(sharedKey); - return ResponseEntity.ok(responses); + @Override + @GetMapping("/boards/{sharedId}") + public ResponseEntity> getSharedBoard(@PathVariable String sharedId){ + List response = boardService.findBySharedKey(sharedId); + return ResponseEntity.ok(response); + } + + @GetMapping("/boards/{sharedId}/detail/{id}") + public ResponseEntity getSharedBoardDetail(@PathVariable String sharedId,@PathVariable Long id){ + StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findSharedBoardDetail(sharedId, id); + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java b/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java deleted file mode 100644 index 3708748..0000000 --- a/src/main/java/com/ddd/moodof/adapter/presentation/SharedController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.ddd.moodof.adapter.presentation; - -import com.ddd.moodof.adapter.presentation.api.SharedAPI; -import com.ddd.moodof.application.BoardService; -import com.ddd.moodof.application.dto.SharedDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; -import java.net.URI; - -@RequiredArgsConstructor -@RequestMapping(SharedController.API_SHARED) -@RestController -public class SharedController implements SharedAPI { - public static final String API_SHARED = "/api/shared"; - public static final String BOARDS = "/boards"; - - private final BoardService boardService; - - @PostMapping(BOARDS) - public ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest){ - SharedDTO.SharedBoardResponse response = boardService.createSharedKey(request.getId(), userId, httpServletRequest); - return ResponseEntity.created(URI.create(API_SHARED + BOARDS + "/" + response.getId())).body(response); - } - -} diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java index 008a2e0..3fb5055 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java @@ -2,11 +2,14 @@ import com.ddd.moodof.adapter.presentation.LoginUserId; import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.SharedDTO; import io.swagger.annotations.ApiImplicitParam; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import springfox.documentation.annotations.ApiIgnore; +import javax.servlet.http.HttpServletRequest; + public interface BoardAPI { @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @PostMapping @@ -23,4 +26,8 @@ public interface BoardAPI { @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") @DeleteMapping("/{id}") ResponseEntity delete(@ApiIgnore @LoginUserId Long userId, @PathVariable Long id); + + @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") + @PostMapping("/shared") + ResponseEntity create(@RequestBody BoardDTO.BoardSharedRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java index 4d55cda..0351ce3 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java @@ -1,15 +1,20 @@ package com.ddd.moodof.adapter.presentation.api; - -import com.ddd.moodof.adapter.presentation.PublicController; -import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.adapter.presentation.LoginUserId; import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.dto.StoragePhotoDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import java.util.List; public interface PublicAPI { - @GetMapping(PublicController.BOARDS) - ResponseEntity> getSharedBoard(@PathVariable String sharedKey); + @GetMapping("/boards/{sharedId}") + ResponseEntity> getSharedBoard(@PathVariable String sharedId); + + @GetMapping("/boards/{sharedId}/detail/{id}") + ResponseEntity getSharedBoardDetail(@PathVariable String sharedId,@PathVariable Long id); + + } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java index b86f88b..62eea04 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java @@ -4,14 +4,20 @@ import com.ddd.moodof.application.dto.SharedDTO; import io.swagger.annotations.ApiImplicitParam; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import javax.servlet.http.HttpServletRequest; public interface SharedAPI { - @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") - @PostMapping - ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); +// @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") +// @PostMapping +// ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); + +// @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") +// @GetMapping("/boards") +// ResponseEntity getSharedKey(@PathVariable Long id, @LoginUserId Long userId); } diff --git a/src/main/java/com/ddd/moodof/application/BoardService.java b/src/main/java/com/ddd/moodof/application/BoardService.java index b4e690c..90af2eb 100644 --- a/src/main/java/com/ddd/moodof/application/BoardService.java +++ b/src/main/java/com/ddd/moodof/application/BoardService.java @@ -3,24 +3,20 @@ import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; import com.ddd.moodof.application.dto.BoardDTO; -import com.ddd.moodof.application.dto.CategoryDTO; -import com.ddd.moodof.application.dto.SharedDTO; import com.ddd.moodof.application.verifier.BoardVerifier; import com.ddd.moodof.domain.model.board.Board; import com.ddd.moodof.domain.model.board.BoardSharedKeyUpdater; import com.ddd.moodof.domain.model.board.BoardRepository; import com.ddd.moodof.domain.model.board.BoardSequenceUpdater; -import com.ddd.moodof.domain.model.category.Category; +import com.ddd.moodof.domain.model.category.CategoryQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; - import javax.servlet.http.HttpServletRequest; import javax.transaction.Transactional; import java.security.GeneralSecurityException; -import java.util.List; @RequiredArgsConstructor @Service @@ -34,10 +30,13 @@ public class BoardService { private final BoardSharedKeyUpdater boardSharedKeyUpdater; + private final CategoryQueryRepository categoryQueryRepository; + private final EncryptConfig encryptConfig; public static final String LOCALHOST = "localhost"; + @Transactional public BoardDTO.BoardResponse create(Long userId, BoardDTO.CreateBoard request) { Board board = boardVerifier.toEntity(request.getPreviousBoardId(), request.getCategoryId(), request.getName(), userId); @@ -74,61 +73,40 @@ public void delete(Long userId, Long id) { boardRepository.deleteById(id); } - public SharedDTO.SharedBoardResponse createSharedKey(Long id, Long userId, HttpServletRequest httpServletRequest) { - //todo 암호화된 값을 board로 등록시킨다 + public BoardDTO.BoardSharedResponse createSharedKey(Long id, Long userId, HttpServletRequest httpServletRequest) { String requestURL= getUrl(httpServletRequest); - String sharedKey = null; + String sharedKey = generatedKey(id); + Board board = boardRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Board, id = " + id)); + return createBoardsURL(id, requestURL, sharedKey, board , userId); + } + + private String generatedKey(Long id) { try { - sharedKey = EncryptUtil.encryptAES256(String.valueOf(id), encryptConfig.getKey()); + return EncryptUtil.encryptAES256(String.valueOf(id), encryptConfig.getKey()); } catch (GeneralSecurityException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } - - Board board = boardRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Board, id = " + id)); - boardSharedKeyUpdater.update(board, sharedKey, userId); - return createBoardsURL(id, requestURL, sharedKey); - } - - public SharedDTO.SharedBoardResponse createBoardsURL(Long id, String requestURL, String sharedKey) { - return generatedURL(id, requestURL, sharedKey); + return null; } -// public List findBySharedKey(String sharedKey) { -// Board board = boardRepository.findBySharedKey(sharedKey) -// .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey)); -//// List totalCategories = categoryRepository.findAllByUserId(board.getUserId()); -//// return CategoryDTO.CategoryResponse.listForm(totalCategories); -// } - - - private SharedDTO.SharedBoardResponse generatedURL(Long id, String requestURL, String sharedKey){ - StringBuilder sharedURL = new StringBuilder(); - sharedURL.append(requestURL) - .append("/") - .append(sharedKey); - return SharedDTO.SharedBoardResponse.from(id,sharedURL.toString(), sharedKey); - } - - public String getUrl(HttpServletRequest request) { ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request); UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(httpRequest).build(); - String scheme = uriComponents.getScheme(); // http / https - String serverName = request.getServerName(); // hostname.com - int serverPort = request.getServerPort(); // 80 - String contextPath = request.getContextPath(); // /app - - // Reconstruct original requesting URL + String scheme = uriComponents.getScheme(); + String serverName = request.getServerName(); + int serverPort = request.getServerPort(); + String contextPath = request.getRequestURI(); StringBuilder url = new StringBuilder(); + url.append(scheme).append("://"); url.append(serverName); if (serverName.equals(LOCALHOST)) { - url.append(":").append(serverPort); + url.append(":").append("8080"); }else { if (serverPort != 80 && serverPort != 443) { url.append(":").append(serverPort); @@ -137,4 +115,18 @@ public String getUrl(HttpServletRequest request) { url.append(contextPath); return url.toString(); } + + public BoardDTO.BoardSharedResponse createBoardsURL(Long id, String requestURL, String sharedKey, Board board, Long userId) { + String sharedURL = generatedURL(requestURL, sharedKey); + boardSharedKeyUpdater.update(board, sharedURL, sharedKey, userId); + return BoardDTO.BoardSharedResponse.from(id,sharedURL, sharedKey); + } + + private String generatedURL(String requestURL, String sharedKey){ + StringBuilder sharedURL = new StringBuilder(); + sharedURL.append(requestURL) + .append("/") + .append(sharedKey); + return sharedURL.toString(); + } } diff --git a/src/main/java/com/ddd/moodof/application/CategoryService.java b/src/main/java/com/ddd/moodof/application/CategoryService.java index 148f4ae..cc29423 100644 --- a/src/main/java/com/ddd/moodof/application/CategoryService.java +++ b/src/main/java/com/ddd/moodof/application/CategoryService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import javax.transaction.Transactional; +import java.util.List; import java.util.Optional; @RequiredArgsConstructor diff --git a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java index 2c58c92..5d287b4 100644 --- a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java +++ b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java @@ -2,6 +2,8 @@ import com.ddd.moodof.adapter.infrastructure.persistence.PaginationUtils; import com.ddd.moodof.application.dto.StoragePhotoDTO; +import com.ddd.moodof.domain.model.board.Board; +import com.ddd.moodof.domain.model.board.BoardRepository; import com.ddd.moodof.domain.model.storage.photo.StoragePhoto; import com.ddd.moodof.domain.model.storage.photo.StoragePhotoCreator; import com.ddd.moodof.domain.model.storage.photo.StoragePhotoQueryRepository; @@ -19,6 +21,7 @@ public class StoragePhotoService { private final StoragePhotoQueryRepository storagePhotoQueryRepository; private final StoragePhotoCreator storagePhotoCreator; private final PaginationUtils paginationUtils; + private final BoardRepository boardRepository; public StoragePhotoDTO.StoragePhotoResponse create(StoragePhotoDTO.CreateStoragePhoto request, Long userId) { StoragePhoto saved = storagePhotoCreator.create(request.getUri(), request.getRepresentativeColor(), userId); @@ -29,6 +32,12 @@ public StoragePhotoDTO.StoragePhotoPageResponse findPage(Long userId, int page, return storagePhotoQueryRepository.findPageExcludeTrash(userId, PageRequest.of(page, size, paginationUtils.getSort(sortBy, descending)), tagIds); } + public StoragePhotoDTO.StoragePhotoPageResponse findSharedPage(String sharedKey, int page, int size, String sortBy, boolean descending, List tagIds) { + Board board = boardRepository.findBySharedKey(sharedKey) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey)); + return storagePhotoQueryRepository.findPageExcludeTrash(board.getUserId(), PageRequest.of(page, size, paginationUtils.getSort(sortBy, descending)), tagIds); + } + public boolean existsByIdAndUserId(Long id, Long userId) { return storagePhotoRepository.existsByIdAndUserId(id, userId); } @@ -43,4 +52,10 @@ public void deleteById(Long userId, Long id) { public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long id) { return storagePhotoQueryRepository.findDetail(userId, id); } + + public StoragePhotoDTO.StoragePhotoDetailResponse findSharedBoardDetail(String sharedKey, Long id) { + Board board = boardRepository.findBySharedKey(sharedKey) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey)); + return storagePhotoQueryRepository.findDetail(board.getUserId(), id); + } } diff --git a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java index 494e1db..ebb4c7a 100644 --- a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java @@ -18,10 +18,10 @@ public static class CreateBoard { private String name; public Board toEntity(Long userId) { - return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "", null, null)); + return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "","", null, null)); } public Board setSharedKey(Board board){ - return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); + return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedURL(),board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); } } @@ -35,12 +35,11 @@ public static class BoardResponse { private Long userId; private String name; private Long categoryId; - private String sharedKey; private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; public static BoardResponse from(Board board) { - return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); + return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getCreatedDate(), board.getLastModifiedDate()); } } @@ -58,4 +57,28 @@ public static class ChangeBoardSequence { private Long categoryId; private Long previousBoardId; } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + public static class BoardSharedRequest{ + private Long id; + } + + @NoArgsConstructor + @Getter + @AllArgsConstructor + public static class BoardSharedResponse { + private Long id; + + private String sharedURL; + + private String sharedKey; + + public static BoardDTO.BoardSharedResponse from(Long id, String sharedURL, String sharedKey) { + return new BoardDTO.BoardSharedResponse(id, sharedURL, sharedKey); + } + } + + } diff --git a/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java b/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java index f3b7bf8..7003b21 100644 --- a/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/SharedDTO.java @@ -7,25 +7,5 @@ import java.time.LocalDateTime; public class SharedDTO { - @NoArgsConstructor - @AllArgsConstructor - @Getter - public static class SharedBoardRequest{ - private Long id; - } - @NoArgsConstructor - @Getter - @AllArgsConstructor - public static class SharedBoardResponse { - private Long id; - - private String sharedURL; - - private String sharedKey; - - public static SharedDTO.SharedBoardResponse from(Long id, String sharedURL, String sharedKey) { - return new SharedDTO.SharedBoardResponse(id, sharedURL, sharedKey); - } - } } diff --git a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java index d34db48..f1678ce 100644 --- a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java +++ b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java @@ -19,6 +19,6 @@ public Board toEntity(Long previousBoardId, Long categoryId, String name, Long u throw new IllegalArgumentException("카테고리 생성자와 로그인 유저의 아이디가 다릅니다."); } - return new Board(null, previousBoardId, userId, name, categoryId, null, null, null); + return new Board(null, previousBoardId, userId, name, categoryId, null, null, null, null); } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/Board.java b/src/main/java/com/ddd/moodof/domain/model/board/Board.java index ec46792..85d8d4c 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/Board.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/Board.java @@ -29,6 +29,8 @@ public class Board { private Long categoryId; + private String sharedURL; + private String sharedKey; @CreatedDate @@ -56,12 +58,12 @@ public void updateSequence(Long previousBoardId, Long categoryId, Long userId) { this.categoryId = categoryId; } - public void updateSharedkey(String sharedKey, Long userId){ + public void updateSharedkey(String sharedURL, String sharedKey, Long userId){ verify(userId); + this.sharedURL = sharedURL; this.sharedKey = sharedKey; } - private void verify(Long userId) { if (isUserNotEqual(userId)) { throw new IllegalArgumentException("userId가 일치하지 않습니다."); diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java index 473a724..1ff392c 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java @@ -16,4 +16,5 @@ public interface BoardRepository extends JpaRepository { void deleteAllByCategoryId(Long categoryId); boolean existsByIdAndUserId(Long id, Long userId); + } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java index c18a9d0..ed95eb0 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java @@ -7,8 +7,8 @@ @Service public class BoardSharedKeyUpdater { private final BoardRepository boardRepository; - public Board update(Board board, String sharedKey, Long userId){ - board.updateSharedkey(sharedKey, userId); + public Board update(Board board, String sharedURL, String sharedKey, Long userId){ + board.updateSharedkey(sharedURL, sharedKey, userId); return boardRepository.save(board); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 04ae6a6..b9fc664 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,4 @@ spring: web-allow-others: true logging.level: - org.hibernate.SQL: debug -server: - port: 80 \ No newline at end of file + org.hibernate.SQL: debug \ No newline at end of file diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index 9974674..dd0cbf7 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -1,7 +1,6 @@ package com.ddd.moodof.acceptance; import com.ddd.moodof.adapter.infrastructure.security.TokenProvider; -import com.ddd.moodof.adapter.presentation.SharedController; import com.ddd.moodof.application.dto.*; import com.ddd.moodof.domain.model.user.AuthProvider; import com.ddd.moodof.domain.model.user.User; @@ -41,7 +40,6 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class AcceptanceTest { private static final String BEARER = "Bearer "; - private static final String BOARDS = "/boards"; protected Long userId; @@ -75,10 +73,9 @@ protected User signUp() { return userRepository.save(new User(null, "test@test.com", "password", "nickname", "profileUrl", null, null, AuthProvider.google, "providerId")); } - protected SharedDTO.SharedBoardResponse 보드_공유하기(Long id, Long userId){ - SharedDTO.SharedBoardRequest request = new SharedDTO.SharedBoardRequest(id); - - return postWithLogin(request, SharedController.API_SHARED + BOARDS, SharedDTO.SharedBoardResponse.class, userId); + protected BoardDTO.BoardSharedResponse 보드_공유하기_생성(Long id, Long userId){ + BoardDTO.BoardSharedRequest request = new BoardDTO.BoardSharedRequest(id); + return postWithLogin(request, API_BOARD+ "/shared", BoardDTO.BoardSharedResponse.class, userId); } protected CategoryDTO.CategoryResponse 카테고리_생성(Long userId, String title, Long previousId){ @@ -233,30 +230,28 @@ protected List getListWithLogin(String uri, Class response, Long userI throw new AssertionError("test fails"); } } - protected T getPublicWithLogin(String uri, Class response) { + protected List getListNotLoginWithProperty(String uri, Class response, String property) { try { - MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri) + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri + "/{property}", property) .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); + CollectionType collectionType = objectMapper.getTypeFactory().constructCollectionType(List.class, response); - return objectMapper.readValue(result.getResponse().getContentAsString(), response); + return objectMapper.readValue(result.getResponse().getContentAsString(), collectionType); } catch (Exception e) { log.error(e.getMessage()); throw new AssertionError("test fails"); } } - - protected List getPublicListWithLogin(String uri, Class response, String resourceId) { + protected T getNotLoginWithMultiProperty(String uri, Class response, String property1, Long property2) { try { - MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri ,resourceId) + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri, property1, property2) .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); - CollectionType collectionType = objectMapper.getTypeFactory().constructCollectionType(List.class, response); - - return objectMapper.readValue(result.getResponse().getContentAsString(), collectionType); + return objectMapper.readValue(result.getResponse().getContentAsString(), response); } catch (Exception e) { log.error(e.getMessage()); throw new AssertionError("test fails"); diff --git a/src/test/java/com/ddd/moodof/acceptance/BoardAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/BoardAcceptanceTest.java index 569ca4d..57307d5 100644 --- a/src/test/java/com/ddd/moodof/acceptance/BoardAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/BoardAcceptanceTest.java @@ -1,7 +1,5 @@ package com.ddd.moodof.acceptance; - -import com.ddd.moodof.application.dto.BoardDTO; -import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.dto.*; import com.ddd.moodof.domain.model.board.Board; import com.ddd.moodof.domain.model.board.BoardRepository; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java new file mode 100644 index 0000000..c6aacc9 --- /dev/null +++ b/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java @@ -0,0 +1,143 @@ +package com.ddd.moodof.acceptance; + +import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; +import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; +import com.ddd.moodof.application.dto.BoardDTO; +import com.ddd.moodof.application.dto.CategoryDTO; +import com.ddd.moodof.application.dto.StoragePhotoDTO; +import com.ddd.moodof.application.dto.TagDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Collections; +import java.util.List; + +import static com.ddd.moodof.adapter.presentation.PublicController.API_PUBLIC; +import static com.ddd.moodof.adapter.presentation.StoragePhotoController.API_STORAGE_PHOTO; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class BoardSharedAcceptanceTest extends AcceptanceTest{ + private StoragePhotoDTO.StoragePhotoResponse storagePhoto; + private BoardDTO.BoardResponse board; + private CategoryDTO.CategoryResponse category; + private CategoryDTO.CategoryResponse category2; + + @Autowired + private EncryptConfig encryptConfig; + + @Override + @BeforeEach + void setUp() { + super.setUp(); + storagePhoto = 보관함사진_생성(userId, "photoUri", "representativeColor"); + category = 카테고리_생성(userId, "title", 0L); + category2 = 카테고리_생성(userId, "title", category.getId()); + board = 보드_생성(userId, 0L, category.getId(), "name"); + } + @Test + public void 공유_URL_생성() throws Exception { + // given + + // when + BoardDTO.BoardSharedResponse response = 보드_공유하기_생성(board.getId(), userId); + Long responseDecrypt = Long.parseLong(EncryptUtil.decryptAES256(response.getSharedKey(), encryptConfig.getKey())); + + // then + assertAll( + () -> assertThat(board.getCategoryId()).isEqualTo(category.getId()), + () -> assertThat(response.getId()).isEqualTo(board.getId()), + () -> assertThat(responseDecrypt).isEqualTo(board.getId()) + ); + } + + @Test + public void 공유_카테고리_보드_리스트_조회() throws Exception { + // given + BoardDTO.BoardSharedResponse shared = 보드_공유하기_생성(board.getId(), userId); + + // when + List response = getListNotLoginWithProperty(API_PUBLIC + "/boards", CategoryDTO.CategoryWithBoardResponse.class, shared.getSharedKey()); + + // then + assertAll( + () -> assertThat(response.get(0).getBoardList().get(0).getId()).isEqualTo(board.getId()), + () -> assertThat(response.get(0).getBoardList().get(0).getCategoryId()).isEqualTo(board.getCategoryId()), + () -> assertThat(response.get(0).getBoardList().get(0).getUserId()).isEqualTo(board.getUserId()), + () -> assertThat(response.get(0).getBoardList().get(0).getName()).isEqualTo(board.getName()), + () -> assertThat(response.get(0).getBoardList().get(0).getPreviousBoardId()).isEqualTo(board.getPreviousBoardId()) + + ); + } + + @Test + void 사진보관함_페이지_조회() { + // given + 보관함사진_생성(userId, "1", "1"); + 보관함사진_생성(userId, "2", "2"); + StoragePhotoDTO.StoragePhotoResponse second = 보관함사진_생성(userId, "3", "3"); + StoragePhotoDTO.StoragePhotoResponse trash = 보관함사진_생성(userId, "4", "4"); + StoragePhotoDTO.StoragePhotoResponse top = 보관함사진_생성(userId, "5", "5"); + 보관함사진_휴지통_이동(Collections.singletonList(trash.getId()), userId); + + // when + String uri = UriComponentsBuilder.fromUriString(API_STORAGE_PHOTO) + .queryParam("page", 0) + .queryParam("size", 3) + .queryParam("sortBy", "lastModifiedDate") + .queryParam("descending", "true") + .build().toUriString(); + + StoragePhotoDTO.StoragePhotoPageResponse response = getWithLogin(uri, StoragePhotoDTO.StoragePhotoPageResponse.class, userId); + + // then + assertAll( + () -> assertThat(response.getStoragePhotos().size()).isEqualTo(3), + () -> assertThat(response.getStoragePhotos().get(0)).usingRecursiveComparison().isEqualTo(top), + () -> assertThat(response.getStoragePhotos().get(1)).usingRecursiveComparison().isEqualTo(second), + () -> assertThat(response.getTotalPageCount()).isEqualTo(2) + ); + } + + @Test + void 공유_보관함사진_상세_조회() { + // given + StoragePhotoDTO.StoragePhotoResponse storagePhoto1 = 보관함사진_생성(userId, "1", "1"); + StoragePhotoDTO.StoragePhotoResponse storagePhoto2 = 보관함사진_생성(userId, "2", "2"); + StoragePhotoDTO.StoragePhotoResponse storagePhoto3 = 보관함사진_생성(userId, "3", "3"); + StoragePhotoDTO.StoragePhotoResponse storagePhoto4 = 보관함사진_생성(userId, "4", "4"); + StoragePhotoDTO.StoragePhotoResponse storagePhoto5 = 보관함사진_생성(userId, "5", "5"); + + BoardDTO.BoardResponse board1 = 보드_생성(userId, 0L, category.getId(), "board-1"); + BoardDTO.BoardResponse board2 = 보드_생성(userId, board1.getId(), category.getId(), "board-2"); + BoardDTO.BoardResponse board3 = 보드_생성(userId, 0L, category2.getId(), "board-3"); + 보드_사진_생성(userId, storagePhoto1.getId(), board1.getId()); + 보드_사진_생성(userId, storagePhoto2.getId(), board1.getId()); + 보드_사진_생성(userId, storagePhoto2.getId(), board2.getId()); + 보드_사진_생성(userId, storagePhoto2.getId(), board3.getId()); + 보드_사진_생성(userId, storagePhoto3.getId(), board1.getId()); + 보드_사진_생성(userId, storagePhoto4.getId(), board2.getId()); + TagDTO.TagResponse tag1 = 태그_생성(userId, "tag-1"); + TagDTO.TagResponse tag2 = 태그_생성(userId, "tag-2"); + 태그붙이기_생성(userId, storagePhoto2.getId(), tag1.getId()); + 태그붙이기_생성(userId, storagePhoto2.getId(), tag2.getId()); + 보관함사진_휴지통_이동(List.of(storagePhoto3.getId(), storagePhoto4.getId()), userId); + BoardDTO.BoardSharedResponse shared = 보드_공유하기_생성(board2.getId(), userId); + + // when + StoragePhotoDTO.StoragePhotoDetailResponse response = getNotLoginWithMultiProperty(API_PUBLIC + "/boards/{property1}/detail/{property2}", StoragePhotoDTO.StoragePhotoDetailResponse.class, shared.getSharedKey(), storagePhoto2.getId()); + + // then + assertAll( + () -> assertThat(response.getId()).isEqualTo(storagePhoto2.getId()), + () -> assertThat(response.getCreatedDate()).isEqualTo(storagePhoto2.getCreatedDate()), + () -> assertThat(response.getLastModifiedDate()).isEqualTo(storagePhoto2.getLastModifiedDate()), + () -> assertThat(response.getPreviousStoragePhotoId()).isEqualTo(storagePhoto5.getId()), + () -> assertThat(response.getNextStoragePhotoId()).isEqualTo(storagePhoto1.getId()), + () -> assertThat(response.getCategories().size()).isEqualTo(2L), + () -> assertThat(response.getTags().size()).isEqualTo(2L) + ); + } +} diff --git a/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java deleted file mode 100644 index fee68ca..0000000 --- a/src/test/java/com/ddd/moodof/acceptance/SharedAcceptanceTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.ddd.moodof.acceptance; - -import com.ddd.moodof.adapter.presentation.PublicController; -import com.ddd.moodof.application.dto.BoardDTO; -import com.ddd.moodof.application.dto.CategoryDTO; -import com.ddd.moodof.application.dto.SharedDTO; -import org.junit.jupiter.api.Test; - -import java.util.List; - - -public class SharedAcceptanceTest extends AcceptanceTest{ - @Test - public void 공유_URL_생성() throws Exception { - // given - long previousBoardId = 0L; - String name = "name"; - CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); - BoardDTO.BoardResponse response = 보드_생성(userId, previousBoardId, category.getId(), name); - - // when - SharedDTO.SharedBoardResponse response1 = 보드_공유하기(response.getId(), userId); - - // then - - } - @Test - public void 권한없이_보드_조회() throws Exception { - // given - long previousBoardId = 0L; - String name = "name"; - CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); - BoardDTO.BoardResponse board = 보드_생성(userId, previousBoardId, category.getId(), name); - SharedDTO.SharedBoardResponse response = 보드_공유하기(board.getId(), userId); - - List publicListWithLogin = getPublicListWithLogin(PublicController.API_PUBLIC + PublicController.BOARDS, BoardDTO.BoardResponse.class, response.getSharedKey()); - - // when - - // then - } -} From d81572f77d4847f300f506edce3e8ad6b01b0786 Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Sat, 29 May 2021 12:30:15 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore:=20profile=20dev=20ignore=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d9d3c22..07d3c7a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ out/ application-security.yml /src/main/resources/application-encrypto.yml +/src/main/resources/application-dev.yml From ae231f1e3ad9e465963b13f72be24dbdaba375f6 Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Sat, 29 May 2021 18:21:20 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/SecurityConfig.java | 1 - .../adapter/presentation/api/PublicAPI.java | 7 ++---- .../adapter/presentation/api/SharedAPI.java | 23 ------------------- 3 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java index 623dcbe..4c84753 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/SecurityConfig.java @@ -54,7 +54,6 @@ public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) } @Override public void configure(WebSecurity web) throws Exception { - //@formatter:off super.configure(web); web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java index 939dd1f..f73caff 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java @@ -14,11 +14,8 @@ public interface PublicAPI { @GetMapping("/boards/{sharedKey}") ResponseEntity> findAllByBoard(@PathVariable String sharedKey); -// @GetMapping("/boards/{sharedId}") -// ResponseEntity> getSharedBoard(@PathVariable String sharedId); - - @GetMapping("/boards/{sharedId}/detail/{id}") - ResponseEntity getSharedBoardDetail(@PathVariable String sharedId,@PathVariable Long id); + @GetMapping("/boards/{sharedKey}/detail/{id}") + ResponseEntity getSharedBoardDetail(@PathVariable String sharedKey,@PathVariable Long id); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java deleted file mode 100644 index 62eea04..0000000 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/SharedAPI.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.ddd.moodof.adapter.presentation.api; - -import com.ddd.moodof.adapter.presentation.LoginUserId; -import com.ddd.moodof.application.dto.SharedDTO; -import io.swagger.annotations.ApiImplicitParam; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; - -import javax.servlet.http.HttpServletRequest; - -public interface SharedAPI { -// @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") -// @PostMapping -// ResponseEntity create(@RequestBody SharedDTO.SharedBoardRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); - -// @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") -// @GetMapping("/boards") -// ResponseEntity getSharedKey(@PathVariable Long id, @LoginUserId Long userId); - -} From d45907a390387a6b9cc3f78d5a7d9292aa41b8df Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Sat, 29 May 2021 18:23:26 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20git=20ignore=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=BD=94=EB=93=9C=20=EC=B0=B8=EC=A1=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 07d3c7a..2c98cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,4 @@ out/ .vscode/ application-security.yml -/src/main/resources/application-encrypto.yml /src/main/resources/application-dev.yml From 8f228f9ea000fd587001980dd1bcb14fe40883fc Mon Sep 17 00:00:00 2001 From: gwanhyeon Date: Mon, 31 May 2021 15:42:36 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9C=A0=20URI=20AES256?= =?UTF-8?q?=20->=20SHA256=20=EC=95=94=ED=98=B8=ED=99=94=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0=20=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/EncryptConfig.java | 14 ---- .../configuration/WebMvcConfig.java | 4 -- .../StoragePhotoQuerydslRepository.java | 1 + .../SharedBoardIdArgumentResolver.java | 46 ------------- .../security/encrypt/EncryptUtil.java | 63 +++++------------- .../adapter/presentation/BoardController.java | 9 ++- ...roller.java => PublicBoardController.java} | 20 +++--- .../adapter/presentation/api/BoardAPI.java | 3 - .../{PublicAPI.java => PublicBoardAPI.java} | 11 ++-- .../ddd/moodof/application/BoardService.java | 65 +++++-------------- .../application/StoragePhotoService.java | 4 +- .../ddd/moodof/application/dto/BoardDTO.java | 9 +-- .../application/verifier/BoardVerifier.java | 2 +- .../ddd/moodof/domain/model/board/Board.java | 5 +- .../domain/model/board/BoardRepository.java | 2 + .../model/board/BoardSharedKeyUpdater.java | 14 ---- .../ddd/moodof/acceptance/AcceptanceTest.java | 4 +- .../acceptance/BoardSharedAcceptanceTest.java | 57 +++++++++++----- 18 files changed, 107 insertions(+), 226 deletions(-) delete mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java delete mode 100644 src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java rename src/main/java/com/ddd/moodof/adapter/presentation/{PublicController.java => PublicBoardController.java} (63%) rename src/main/java/com/ddd/moodof/adapter/presentation/api/{PublicAPI.java => PublicBoardAPI.java} (65%) delete mode 100644 src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java deleted file mode 100644 index 885052b..0000000 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/EncryptConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ddd.moodof.adapter.infrastructure.configuration; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@ConfigurationProperties(prefix = "encrypt") -@Getter -@Setter -public class EncryptConfig { - private String key; -} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java index 5a2ef48..69ef7ac 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/configuration/WebMvcConfig.java @@ -1,7 +1,6 @@ package com.ddd.moodof.adapter.infrastructure.configuration; import com.ddd.moodof.adapter.infrastructure.security.LoginUserIdArgumentResolver; -import com.ddd.moodof.adapter.infrastructure.security.SharedBoardIdArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -17,12 +16,9 @@ public class WebMvcConfig implements WebMvcConfigurer { private final LoginUserIdArgumentResolver loginUserIdArgumentResolver; - private final SharedBoardIdArgumentResolver sharedBoardIdArgumentResolver; - @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginUserIdArgumentResolver); - resolvers.add(sharedBoardIdArgumentResolver); } @Override diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java index ef3b7fc..441736c 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/persistence/querydsl/StoragePhotoQuerydslRepository.java @@ -215,6 +215,7 @@ private List findBoards(Long storagePhotoId) { board.userId, board.name, board.categoryId, + board.sharedKey, board.createdDate, board.lastModifiedDate )) diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java deleted file mode 100644 index 9456d3a..0000000 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/SharedBoardIdArgumentResolver.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.ddd.moodof.adapter.infrastructure.security; - -import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; - -import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; -import com.ddd.moodof.adapter.presentation.SharedBoardId; -import com.ddd.moodof.domain.model.board.BoardRepository; -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; - -@RequiredArgsConstructor -@Component -public class SharedBoardIdArgumentResolver implements HandlerMethodArgumentResolver { - - private final BoardRepository boardRepository; - - private final EncryptConfig encryptConfig; - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(SharedBoardId.class); - } - - @Override - public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - - SharedBoardId customParam = parameter.getParameterAnnotation(SharedBoardId.class); - String key = webRequest.getParameter(customParam.value()); - - try { - String id = EncryptUtil.decryptAES256(key, encryptConfig.getKey()); - Long boardId = Long.valueOf(id); - boardRepository.findById(boardId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id = " + id)); - return boardId; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java index 8341960..ab8a1a6 100644 --- a/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java +++ b/src/main/java/com/ddd/moodof/adapter/infrastructure/security/encrypt/EncryptUtil.java @@ -1,59 +1,26 @@ package com.ddd.moodof.adapter.infrastructure.security.encrypt; import org.springframework.stereotype.Component; -import javax.crypto.*; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import java.nio.ByteBuffer; -import java.security.AlgorithmParameters; -import java.security.SecureRandom; -import java.util.Base64; +import java.security.MessageDigest; @Component public class EncryptUtil { - public static String encryptAES256(String msg, String key) throws Exception { - SecureRandom random = new SecureRandom(); - byte bytes[] = new byte[20]; - random.nextBytes(bytes); - byte[] saltBytes = bytes; - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256); - SecretKey secretKey = factory.generateSecret(spec); - SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + public static String encryptSHA256(String msg){ + try{ + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(msg.getBytes("UTF-8")); + StringBuffer hexString = new StringBuffer(); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, secret); - AlgorithmParameters params = cipher.getParameters(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } - byte[] ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV(); - byte[] encryptedTextBytes = cipher.doFinal(msg.getBytes("UTF-8")); - byte[] buffer = new byte[saltBytes.length + ivBytes.length + encryptedTextBytes.length]; - System.arraycopy(saltBytes, 0, buffer, 0, saltBytes.length); - System.arraycopy(ivBytes, 0, buffer, saltBytes.length, ivBytes.length); - System.arraycopy(encryptedTextBytes, 0, buffer, saltBytes.length + ivBytes.length, encryptedTextBytes.length); -// .replaceAll("[^0-9a-zA-Z]", ""); - return Base64.getEncoder().encodeToString(buffer).replaceAll("\\/","%"); - } - public static String decryptAES256(String msg, String key) throws Exception { - msg = msg.replaceAll("%","/"); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - ByteBuffer buffer = ByteBuffer.wrap(Base64.getDecoder().decode(msg)); - byte[] saltBytes = new byte[20]; - byte[] ivBytes = new byte[cipher.getBlockSize()]; - byte[] encryoptedTextBytes = new byte[buffer.capacity() - saltBytes.length - ivBytes.length]; - - buffer.get(saltBytes, 0, saltBytes.length); - buffer.get(ivBytes, 0, ivBytes.length); - buffer.get(encryoptedTextBytes); - - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(key.toCharArray(), saltBytes, 70000, 256); - SecretKey secretKey = factory.generateSecret(spec); - SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); - cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes)); - byte[] decryptedTextBytes = cipher.doFinal(encryoptedTextBytes); - return new String(decryptedTextBytes); + return hexString.toString(); + } catch(Exception ex){ + throw new RuntimeException(ex); + } } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java b/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java index caa58ed..57acb33 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/BoardController.java @@ -46,10 +46,9 @@ public ResponseEntity delete(@LoginUserId Long userId, @ boardService.delete(userId, id); return ResponseEntity.noContent().build(); } - - @PostMapping("/shared") - public ResponseEntity create(@RequestBody BoardDTO.BoardSharedRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest){ - BoardDTO.BoardSharedResponse response = boardService.createSharedKey(request.getId(), userId, httpServletRequest); - return ResponseEntity.created(URI.create(API_BOARD + "/shared/" + response.getId())).body(response); + @GetMapping("/{id}") + public ResponseEntity getSharedKey(@LoginUserId Long userId, @PathVariable Long id, HttpServletRequest httpServletRequest){ + BoardDTO.BoardSharedResponse response = boardService.getSharedURI(userId, id, httpServletRequest); + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java b/src/main/java/com/ddd/moodof/adapter/presentation/PublicBoardController.java similarity index 63% rename from src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java rename to src/main/java/com/ddd/moodof/adapter/presentation/PublicBoardController.java index a43f64b..1813c12 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/PublicController.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/PublicBoardController.java @@ -1,6 +1,6 @@ package com.ddd.moodof.adapter.presentation; -import com.ddd.moodof.adapter.presentation.api.PublicAPI; +import com.ddd.moodof.adapter.presentation.api.PublicBoardAPI; import com.ddd.moodof.application.BoardPhotoService; import com.ddd.moodof.application.StoragePhotoService; import com.ddd.moodof.application.dto.BoardPhotoDTO; @@ -12,26 +12,30 @@ import java.util.List; @RequiredArgsConstructor -@RequestMapping(PublicController.API_PUBLIC) +@RequestMapping(PublicBoardController.API_PUBLIC_BOARDS) @RestController -public class PublicController implements PublicAPI { +public class PublicBoardController implements PublicBoardAPI { - public static final String API_PUBLIC = "/api/public"; + public static final String API_PUBLIC_BOARDS = "/api/public/boards"; private final StoragePhotoService storagePhotoService; private final BoardPhotoService boardPhotoService; @Override - @GetMapping("/boards/{sharedKey}") + @GetMapping("/{sharedKey}") public ResponseEntity> findAllByBoard(@PathVariable String sharedKey) { List responses = boardPhotoService.findAllBySharedKey(sharedKey); return ResponseEntity.ok(responses); } - @GetMapping("/boards/{sharedKey}/detail/{id}") - public ResponseEntity getSharedBoardDetail(@PathVariable String sharedKey,@PathVariable Long id){ - StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findSharedBoardDetail(sharedKey, id); + @Override + @GetMapping("/{sharedKey}/detail/{id}") + public ResponseEntity getSharedBoardDetail( + @PathVariable String sharedKey, + @PathVariable Long id, + @RequestParam(required = false, value = "tagIds") List tagIds){ + StoragePhotoDTO.StoragePhotoDetailResponse response = storagePhotoService.findSharedBoardDetail(sharedKey, id, tagIds); return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java index 3fb5055..4e5be29 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/BoardAPI.java @@ -27,7 +27,4 @@ public interface BoardAPI { @DeleteMapping("/{id}") ResponseEntity delete(@ApiIgnore @LoginUserId Long userId, @PathVariable Long id); - @ApiImplicitParam(name = "Authorization", value = "Access Token", required = true, paramType = "header", dataTypeClass = String.class, example = "Bearer access_token") - @PostMapping("/shared") - ResponseEntity create(@RequestBody BoardDTO.BoardSharedRequest request, @LoginUserId Long userId, HttpServletRequest httpServletRequest); } diff --git a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicBoardAPI.java similarity index 65% rename from src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java rename to src/main/java/com/ddd/moodof/adapter/presentation/api/PublicBoardAPI.java index f73caff..879fe0c 100644 --- a/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicAPI.java +++ b/src/main/java/com/ddd/moodof/adapter/presentation/api/PublicBoardAPI.java @@ -1,7 +1,5 @@ package com.ddd.moodof.adapter.presentation.api; -import com.ddd.moodof.adapter.presentation.LoginUserId; import com.ddd.moodof.application.dto.BoardPhotoDTO; -import com.ddd.moodof.application.dto.CategoryDTO; import com.ddd.moodof.application.dto.StoragePhotoDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -10,12 +8,11 @@ import java.util.List; -public interface PublicAPI { - @GetMapping("/boards/{sharedKey}") +public interface PublicBoardAPI { + @GetMapping("/{sharedKey}") ResponseEntity> findAllByBoard(@PathVariable String sharedKey); - @GetMapping("/boards/{sharedKey}/detail/{id}") - ResponseEntity getSharedBoardDetail(@PathVariable String sharedKey,@PathVariable Long id); - + @GetMapping("/{sharedKey}/detail/{id}") + ResponseEntity getSharedBoardDetail(@PathVariable String sharedKey, @PathVariable Long id, @RequestParam(required = false, value = "tagIds") List tagIds); } diff --git a/src/main/java/com/ddd/moodof/application/BoardService.java b/src/main/java/com/ddd/moodof/application/BoardService.java index 0bd14ac..9e8b86f 100644 --- a/src/main/java/com/ddd/moodof/application/BoardService.java +++ b/src/main/java/com/ddd/moodof/application/BoardService.java @@ -1,15 +1,11 @@ package com.ddd.moodof.application; -import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; import com.ddd.moodof.application.dto.BoardDTO; -import com.ddd.moodof.application.dto.CategoryDTO; import com.ddd.moodof.application.verifier.BoardVerifier; import com.ddd.moodof.domain.model.board.Board; -import com.ddd.moodof.domain.model.board.BoardSharedKeyUpdater; import com.ddd.moodof.domain.model.board.BoardRepository; import com.ddd.moodof.domain.model.board.BoardSequenceUpdater; -import com.ddd.moodof.domain.model.category.CategoryQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Service; @@ -17,8 +13,6 @@ import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.http.HttpServletRequest; import javax.transaction.Transactional; -import java.security.GeneralSecurityException; -import java.util.List; @RequiredArgsConstructor @Service @@ -30,12 +24,6 @@ public class BoardService { private final BoardSequenceUpdater boardSequenceUpdater; - private final BoardSharedKeyUpdater boardSharedKeyUpdater; - - private final CategoryQueryRepository categoryQueryRepository; - - private final EncryptConfig encryptConfig; - public static final String LOCALHOST = "localhost"; @@ -43,13 +31,19 @@ public class BoardService { public BoardDTO.BoardResponse create(Long userId, BoardDTO.CreateBoard request) { Board board = boardVerifier.toEntity(request.getPreviousBoardId(), request.getCategoryId(), request.getName(), userId); Board saved = boardRepository.save(board); - + encryptByBoardId(userId, saved); boardRepository.findByUserIdAndPreviousBoardIdAndIdNot(userId, request.getPreviousBoardId(), saved.getId()) .ifPresent(it -> it.changePreviousBoardId(saved.getId(), userId)); return BoardDTO.BoardResponse.from(saved); } + public void encryptByBoardId(Long userId, Board saved) { + String sharedKey = EncryptUtil.encryptSHA256(Long.toString(saved.getId())); + saved.updateSharedkey(sharedKey, userId); + boardRepository.save(saved); + } + public BoardDTO.BoardResponse changeName(Long userId, Long id, BoardDTO.ChangeBoardName request) { Board board = boardRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 보드입니다. id = " + id)); @@ -75,23 +69,13 @@ public void delete(Long userId, Long id) { boardRepository.deleteById(id); } - public BoardDTO.BoardSharedResponse createSharedKey(Long id, Long userId, HttpServletRequest httpServletRequest) { - String requestURL= getUrl(httpServletRequest); - String sharedKey = generatedKey(id); - Board board = boardRepository.findById(id) + public BoardDTO.BoardSharedResponse getSharedURI(Long userId, Long id, HttpServletRequest httpServletRequest) { + Board board = boardRepository.findByIdAndUserId(id, userId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Board, id = " + id)); - return createBoardsURL(id, requestURL, sharedKey, board , userId); - } - - private String generatedKey(Long id) { - try { - return EncryptUtil.encryptAES256(String.valueOf(id), encryptConfig.getKey()); - } catch (GeneralSecurityException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - return null; + String requestURI = getUrl(httpServletRequest); + String sharedKey = board.getSharedKey(); + String sharedURI = generatedURI(requestURI, sharedKey); + return BoardDTO.BoardSharedResponse.from(id,sharedURI, sharedKey); } public String getUrl(HttpServletRequest request) { @@ -118,23 +102,10 @@ public String getUrl(HttpServletRequest request) { return url.toString(); } - public BoardDTO.BoardSharedResponse createBoardsURL(Long id, String requestURL, String sharedKey, Board board, Long userId) { - String sharedURL = generatedURL(requestURL, sharedKey); - boardSharedKeyUpdater.update(board, sharedURL, sharedKey, userId); - return BoardDTO.BoardSharedResponse.from(id,sharedURL, sharedKey); - } - - private String generatedURL(String requestURL, String sharedKey){ - StringBuilder sharedURL = new StringBuilder(); - sharedURL.append(requestURL) - .append("/") - .append(sharedKey); - return sharedURL.toString(); - } - - public List findBySharedKey(String sharedKey) { - Board board = boardRepository.findBySharedKey(sharedKey) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 key = " + sharedKey)); - return categoryQueryRepository.findAllByUserId(board.getUserId()); + private String generatedURI(String requestURL, String sharedKey) { + return UriComponentsBuilder.fromUriString(requestURL) + .path(sharedKey) + .build() + .toUriString(); } } diff --git a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java index 10daa75..000072e 100644 --- a/src/main/java/com/ddd/moodof/application/StoragePhotoService.java +++ b/src/main/java/com/ddd/moodof/application/StoragePhotoService.java @@ -54,9 +54,9 @@ public StoragePhotoDTO.StoragePhotoDetailResponse findDetail(Long userId, Long i return storagePhotoQueryRepository.findDetail(userId, id, tagIds); } - public StoragePhotoDTO.StoragePhotoDetailResponse findSharedBoardDetail(String sharedKey, Long id) { + public StoragePhotoDTO.StoragePhotoDetailResponse findSharedBoardDetail(String sharedKey, Long id, List tagIds) { Board board = boardRepository.findBySharedKey(sharedKey) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 sharedKey : " + sharedKey)); - return storagePhotoQueryRepository.findDetail(board.getUserId(), id, new ArrayList<>()); + return storagePhotoQueryRepository.findDetail(board.getUserId(), id, tagIds); } } diff --git a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java index ebb4c7a..841fc26 100644 --- a/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java +++ b/src/main/java/com/ddd/moodof/application/dto/BoardDTO.java @@ -18,10 +18,10 @@ public static class CreateBoard { private String name; public Board toEntity(Long userId) { - return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "","", null, null)); + return setSharedKey(new Board(null, previousBoardId, userId, name, categoryId, "",null, null)); } public Board setSharedKey(Board board){ - return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedURL(),board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); + return new Board(board.getId(),board.getPreviousBoardId(),board.getUserId(), board.getName(),board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); } } @@ -35,11 +35,12 @@ public static class BoardResponse { private Long userId; private String name; private Long categoryId; + private String sharedKey; private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; public static BoardResponse from(Board board) { - return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getCreatedDate(), board.getLastModifiedDate()); + return new BoardResponse(board.getId(), board.getPreviousBoardId(), board.getUserId(), board.getName(), board.getCategoryId(), board.getSharedKey(), board.getCreatedDate(), board.getLastModifiedDate()); } } @@ -71,7 +72,7 @@ public static class BoardSharedRequest{ public static class BoardSharedResponse { private Long id; - private String sharedURL; + private String sharedURI; private String sharedKey; diff --git a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java index f1678ce..d34db48 100644 --- a/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java +++ b/src/main/java/com/ddd/moodof/application/verifier/BoardVerifier.java @@ -19,6 +19,6 @@ public Board toEntity(Long previousBoardId, Long categoryId, String name, Long u throw new IllegalArgumentException("카테고리 생성자와 로그인 유저의 아이디가 다릅니다."); } - return new Board(null, previousBoardId, userId, name, categoryId, null, null, null, null); + return new Board(null, previousBoardId, userId, name, categoryId, null, null, null); } } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/Board.java b/src/main/java/com/ddd/moodof/domain/model/board/Board.java index 85d8d4c..84245d9 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/Board.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/Board.java @@ -29,8 +29,6 @@ public class Board { private Long categoryId; - private String sharedURL; - private String sharedKey; @CreatedDate @@ -58,9 +56,8 @@ public void updateSequence(Long previousBoardId, Long categoryId, Long userId) { this.categoryId = categoryId; } - public void updateSharedkey(String sharedURL, String sharedKey, Long userId){ + public void updateSharedkey(String sharedKey, Long userId){ verify(userId); - this.sharedURL = sharedURL; this.sharedKey = sharedKey; } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java index b67add3..a21daea 100644 --- a/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java +++ b/src/main/java/com/ddd/moodof/domain/model/board/BoardRepository.java @@ -20,4 +20,6 @@ public interface BoardRepository extends JpaRepository { Optional findByUserIdAndPreviousBoardIdAndIdNot(Long userId, Long previousBoardId, Long id); Optional findByUserIdAndPreviousBoardId(Long userId, Long id); + + Optional findByIdAndUserId(Long id, Long userId); } diff --git a/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java b/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java deleted file mode 100644 index ed95eb0..0000000 --- a/src/main/java/com/ddd/moodof/domain/model/board/BoardSharedKeyUpdater.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ddd.moodof.domain.model.board; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class BoardSharedKeyUpdater { - private final BoardRepository boardRepository; - public Board update(Board board, String sharedURL, String sharedKey, Long userId){ - board.updateSharedkey(sharedURL, sharedKey, userId); - return boardRepository.save(board); - } -} diff --git a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java index 3617ce0..ee7ea1a 100644 --- a/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/AcceptanceTest.java @@ -295,9 +295,9 @@ protected List getListNotLogin(String uri, Class response, String prop throw new AssertionError("test fails"); } } - protected T getNotLoginWithMultiProperty(String uri, Class response, String property1, Long property2) { + protected T getNotLogin(String uri, Class response) { try { - MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri, property1, property2) + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(uri) .contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); diff --git a/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java b/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java index b4d4ba0..39068e7 100644 --- a/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java +++ b/src/test/java/com/ddd/moodof/acceptance/BoardSharedAcceptanceTest.java @@ -1,37 +1,54 @@ package com.ddd.moodof.acceptance; -import com.ddd.moodof.adapter.infrastructure.configuration.EncryptConfig; -import com.ddd.moodof.adapter.infrastructure.security.encrypt.EncryptUtil; import com.ddd.moodof.application.dto.*; +import com.ddd.moodof.domain.model.board.Board; +import com.ddd.moodof.domain.model.board.BoardRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.util.UriComponentsBuilder; import java.util.List; -import static com.ddd.moodof.adapter.presentation.PublicController.API_PUBLIC; +import static com.ddd.moodof.adapter.presentation.BoardController.API_BOARD; +import static com.ddd.moodof.adapter.presentation.PublicBoardController.API_PUBLIC_BOARDS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; public class BoardSharedAcceptanceTest extends AcceptanceTest{ @Autowired - private EncryptConfig encryptConfig; + private BoardRepository boardRepository; + @Test - public void 공유_URL_생성() throws Exception { + public void 공유_URI_생성() throws Exception { // given CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); - BoardDTO.BoardResponse board = 보드_생성(userId, 0L, category.getId(), "name"); // when - BoardDTO.BoardSharedResponse response = 보드_공유하기_생성(board.getId(), userId); - Long responseDecrypt = Long.parseLong(EncryptUtil.decryptAES256(response.getSharedKey(), encryptConfig.getKey())); + BoardDTO.BoardResponse response = 보드_생성(userId, 0L, category.getId(), "name"); + Board board = boardRepository.findById(response.getId()).get(); // then assertAll( - () -> assertThat(board.getCategoryId()).isEqualTo(category.getId()), () -> assertThat(response.getId()).isEqualTo(board.getId()), - () -> assertThat(responseDecrypt).isEqualTo(board.getId()) + () -> assertThat(response.getCategoryId()).isEqualTo(board.getCategoryId()), + () -> assertThat(response.getSharedKey()).isEqualTo(board.getSharedKey()) ); } + + @Test + public void 공유_URI_조회() throws Exception { + // given + CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); + BoardDTO.BoardResponse board = 보드_생성(userId, 0L, category.getId(), "name"); + + // when + BoardDTO.BoardSharedResponse response = getWithLogin(API_BOARD + "/" + board.getId(), BoardDTO.BoardSharedResponse.class, userId); + + // then + assertThat(response.getSharedKey()).isEqualTo(board.getSharedKey()); + + + } @Test public void 공유_보드_조회() throws Exception { @@ -40,17 +57,17 @@ public class BoardSharedAcceptanceTest extends AcceptanceTest{ StoragePhotoDTO.StoragePhotoResponse storagePhoto2 = 보관함사진_생성(userId, "photoUri", "representativeColor"); CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); BoardDTO.BoardResponse board = 보드_생성(userId, 0L, category.getId(), "name"); + 보드_사진_복수_생성(userId, List.of(storagePhoto.getId(), storagePhoto2.getId()), board.getId()); // when - 보드_사진_복수_생성(userId, List.of(storagePhoto.getId(), storagePhoto2.getId()), board.getId()); - BoardDTO.BoardSharedResponse response = 보드_공유하기_생성(board.getId(), userId); - List responses = getListNotLogin(API_PUBLIC + "/boards" , BoardPhotoDTO.BoardPhotoResponse.class, response.getSharedKey()); + List response = getListNotLogin(API_PUBLIC_BOARDS , BoardPhotoDTO.BoardPhotoResponse.class, board.getSharedKey()); + // then - assertThat(responses.size()).isEqualTo(2); + assertThat(response.size()).isEqualTo(2); } @Test - void 공유_보관함사진_상세_조회() { + void 공유_보드_상세조회() { // given CategoryDTO.CategoryResponse category = 카테고리_생성(userId, "title", 0L); CategoryDTO.CategoryResponse category2 = 카테고리_생성(userId, "title", category.getId()); @@ -75,10 +92,16 @@ public class BoardSharedAcceptanceTest extends AcceptanceTest{ 태그붙이기_생성(userId, storagePhoto2.getId(), tag1.getId()); 태그붙이기_생성(userId, storagePhoto2.getId(), tag2.getId()); 보관함사진_휴지통_이동(List.of(storagePhoto3.getId(), storagePhoto4.getId()), userId); - BoardDTO.BoardSharedResponse shared = 보드_공유하기_생성(board2.getId(), userId); // when - StoragePhotoDTO.StoragePhotoDetailResponse response = getNotLoginWithMultiProperty(API_PUBLIC + "/boards/{property1}/detail/{property2}", StoragePhotoDTO.StoragePhotoDetailResponse.class, shared.getSharedKey(), storagePhoto2.getId()); + String uri = UriComponentsBuilder.fromUriString(API_PUBLIC_BOARDS) + .path("/{property1}/detail/{property2}") + .queryParam("tagIds", 0L, tag1.getId(), tag2.getId()) + .build() + .expand(board2.getSharedKey(), storagePhoto2.getId()) + .toUriString(); + + StoragePhotoDTO.StoragePhotoDetailResponse response = getNotLogin(uri, StoragePhotoDTO.StoragePhotoDetailResponse.class); // then assertAll(