Skip to content

Commit

Permalink
Merge pull request #149 from eod940/054-FEAT-POST
Browse files Browse the repository at this point in the history
feat: [054-FEAT-POST] Post API 기능 구현
  • Loading branch information
eod940 committed Mar 18, 2024
2 parents 84965a2 + d929ce5 commit d182df0
Show file tree
Hide file tree
Showing 32 changed files with 1,300 additions and 16 deletions.
16 changes: 16 additions & 0 deletions src/main/java/com/valuewith/tweaver/auth/CustomAuthPrincipal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.valuewith.tweaver.auth;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomAuthPrincipal {
boolean errorOnInvalidType() default false;

String expression() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.valuewith.tweaver.auth;

import com.valuewith.tweaver.constants.ErrorCode;
import com.valuewith.tweaver.exception.CustomAuthException;
import java.lang.annotation.Annotation;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class CustomAuthenticationPrincipalArgumentResolver implements
HandlerMethodArgumentResolver {

private ExpressionParser parser = new SpelExpressionParser();
private BeanResolver beanResolver;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return findMethodAnnotation(CustomAuthPrincipal.class, parameter) != null;
}

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

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new CustomAuthException(ErrorCode.NO_PRINCIPAL); // [100] 401 unauthorized
}
Object principal = authentication.getPrincipal();
CustomAuthPrincipal annotation = findMethodAnnotation(CustomAuthPrincipal.class, parameter);
String expressionToParse = annotation.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(principal);
context.setVariable("this", principal);
context.setBeanResolver(this.beanResolver);
Expression expression = this.parser.parseExpression(expressionToParse);
principal = expression.getValue(context);
}
if (principal != null && !ClassUtils.isAssignable(parameter.getParameterType(),
principal.getClass())) {
if (annotation.errorOnInvalidType()) {
throw new ClassCastException(
principal + " is not assignable to " + parameter.getParameterType());
}
throw new CustomAuthException(ErrorCode.NO_PRINCIPAL); // [100] 401 unauthorized
}
return principal;
}

private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass,
MethodParameter parameter) {
T annotation = parameter.getParameterAnnotation(annotationClass);
if (annotation != null) {
return annotation;
}
Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
for (Annotation toSearch : annotationsToSearch) {
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
if (annotation != null) {
return annotation;
}
}
return null;
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/valuewith/tweaver/commons/PrincipalDetails.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.valuewith.tweaver.commons;

import com.valuewith.tweaver.constants.ErrorCode;
import com.valuewith.tweaver.exception.CustomAuthException;
import com.valuewith.tweaver.member.entity.Member;
import java.util.Collection;
import java.util.Map;
Expand Down Expand Up @@ -46,6 +48,9 @@ public String getPassword() {

@Override
public String getUsername() {
if (member == null) {
throw new CustomAuthException(ErrorCode.INVALID_CUSTOM_ID); // [002] 401 unauthorized
}
return member.getEmail();
}

Expand All @@ -72,6 +77,16 @@ public boolean isEnabled() {
// OAuth2 인터페이스 메소드
@Override
public String getName() {
if (member == null) {
throw new CustomAuthException(ErrorCode.INVALID_CUSTOM_NAME); // [003] 401 unauthorized
}
return member.getEmail();
}

public Long getId() {
if (member == null) {
throw new CustomAuthException(ErrorCode.INVALID_CUSTOM_ID); // [001] 401 unauthorized
}
return member.getMemberId();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.valuewith.tweaver.commons.security.service;

import static com.valuewith.tweaver.constants.ErrorCode.*;
import static com.valuewith.tweaver.constants.ErrorCode.INVALID_JWT;

import com.valuewith.tweaver.constants.ErrorCode;
import com.valuewith.tweaver.exception.CustomException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
Expand All @@ -15,13 +14,12 @@
import javax.servlet.http.HttpServletResponse;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Slf4j
@Component
@RequiredArgsConstructor
@Getter
Expand Down Expand Up @@ -120,7 +118,7 @@ public Boolean isValidToken(String token) {
Claims claims = parseClaims(token);
return claims.getExpiration().after(new Date());
} catch (Exception e) {
System.out.println("들어온 token: " + token);
log.error("Access Token 값이 잘못되었습니다.");
throw new CustomException(INVALID_JWT);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/member",
"/oauth2/**",
"/calendar/**",
"/bookmark/**"
"/bookmark/**",
"/post",
"/post/**"
)
.permitAll()
// 회원만 들어갈 수 있는 API는 현재 Security에서 거르지 못합니다.
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/valuewith/tweaver/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.valuewith.tweaver.config;

import com.valuewith.tweaver.auth.CustomAuthenticationPrincipalArgumentResolver;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand Down Expand Up @@ -90,4 +93,10 @@ private ApiInfo apiInfo() {
.version("v0.1")
.build();
}

// CustomAuthPrincipal 어노테이션 관련 메서드
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CustomAuthenticationPrincipalArgumentResolver());
}
}
16 changes: 15 additions & 1 deletion src/main/java/com/valuewith/tweaver/constants/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,39 @@ public enum ErrorCode {
LOCATION_NAME_NOT_FOUNT("지역 이름은 필수입니다.", HttpStatus.BAD_REQUEST),
INVALID_PROFILE_MODIFIED_MEMBER("프로필 수정 권한이 없습니다.", HttpStatus.NON_AUTHORITATIVE_INFORMATION),
NOT_MATCH_PASSWORD("입력한 비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST),
// ======== Auth ========
// 401
INVALID_CODE("만료된 코드 입니다.", HttpStatus.UNAUTHORIZED),
INCORRECT_CODE("인증코드가 다릅니다.", HttpStatus.UNAUTHORIZED),
INVALID_JWT("잘못된 인증 정보입니다.", HttpStatus.UNAUTHORIZED),
INVALID_USER_DETAILS("잘못된 접근입니다.", HttpStatus.UNAUTHORIZED),
NOT_A_MEMBER("해당 그룹원이 아닙니다.", HttpStatus.UNAUTHORIZED),
UNVALIDATED_REDIRECT_URI("인증에 실패하였습니다.\n(Unauthorized uri)", HttpStatus.UNAUTHORIZED),
POST_WRITER_NOT_MATCH("해당 글에 접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED),
INVALID_CUSTOM_ID("[001] 잘못된 인증 정보입니다.", HttpStatus.UNAUTHORIZED),
INVALID_CUSTOM_USERNAME("[002] 잘못된 인증 정보입니다.", HttpStatus.UNAUTHORIZED),
INVALID_CUSTOM_NAME("[003] 잘못된 인증 정보입니다.", HttpStatus.UNAUTHORIZED),
NO_PRINCIPAL("[100] 잘못된 인증 정보입니다.", HttpStatus.UNAUTHORIZED),
// 404
CHAT_ROOM_NOT_FOUND("채팅방을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
CHAT_ROOM_NOT_FOUND_FOR_DELETE("삭제하려는 채팅방을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
MEMBER_NOT_FOUND("회원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
GROUP_NOT_FOUND("그룹을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
GROUP_NOT_FOUND_FOR_DELETE("삭제하려는 그룹을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
POST_NOT_FOUND_FOR_UPDATE("수정하려는 글을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
POST_NOT_FOUND_FOR_DELETE("삭제하려는 글을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
// 409
DUPLICATE_EMAIL("중복된 이메일입니다.", HttpStatus.CONFLICT),

// 422
MEMBER_COUNT_CANNOT_BE_NEGATIVE("그룹 멤버는 0명 이하가 될 수 없습니다", HttpStatus.UNPROCESSABLE_ENTITY),
MEMBER_COUNT_MAX("그룹이 이미 최대 인원에 도달했습니다. 더 이상 신청할 수 없습니다.", HttpStatus.UNPROCESSABLE_ENTITY),

// 500
IMAGE_SAVE_ERROR("이미지 저장 과정에서 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),

// 502
FAILURE_SAVE_IMAGE("이미지를 확인해주세요.", HttpStatus.BAD_GATEWAY),
FAILURE_DELETE_IMAGE("이미지 삭제에 실패하였습니다.", HttpStatus.BAD_GATEWAY),
FAILURE_SENDING_EMAIL("이메일 전송에 실패하였습니다.", HttpStatus.BAD_GATEWAY),
FAILURE_GETTING_PROFILE_IMG("프로필 이미지가 없습니다.", HttpStatus.BAD_GATEWAY)
;
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/com/valuewith/tweaver/constants/ImageType.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package com.valuewith.tweaver.constants;

import lombok.Getter;

@Getter
public enum ImageType {

PROFILE("profile/"),
THUMBNAIL("thumbnail/"),
LOCATION("location/"),
MEMBER("member/");
MEMBER("member/"),
POST("post/");

private final String path;


ImageType(String path) {
this.path = path;
}

public String getPath() {
return path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ public class ImageService {
* PROFILE("profile/")
* THUMBNAIL("thumbnail/")
* LOCATION("location/")
* POST("post/")
* 예: user.setProfileUrl(imageService.uploadImageAndGetUrl(file,ImageType.PROFILE))
* 예: location.setThumbnailUrl(imageService.uploadImageAndGetUrl(file, ImageType.THUMBNAIL))
* 예: post.setImageUrl(imageService.uploadImageAndGetUrl(file,ImageType.POST))
*/
public String uploadImageAndGetUrl(MultipartFile file, ImageType imageType) {
if (file.isEmpty()) {
Expand Down Expand Up @@ -153,4 +155,21 @@ public String randomDefaultImageUploadAndGetUrl(MultipartFile file, ImageType im
defaultImageRepository.save(defaultImage);
return imageUrl;
}

public void deleteImageFile(String imageUrl) {
if (imageUrl == null || imageUrl.isBlank()) {
throw new UrlEmptyException(ErrorCode.URL_IS_EMPTY);
}

String imageKey = generateKey(imageUrl);

if (!amazonS3.doesObjectExist(bucketName, imageKey)) {
throw new S3ImageNotFoundException(ErrorCode.S3_IMAGE_NOT_FOUND);
}

boolean exists = defaultImageRepository.existsDefaultImageByImageName(imageUrl);
if(!exists) {
amazonS3.deleteObject(bucketName, imageKey);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.valuewith.tweaver.exception;

import com.valuewith.tweaver.constants.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public class CustomAuthException extends RuntimeException {

private final ErrorCode errorCode;
private final String message;
private final HttpStatus httpStatus;

public CustomAuthException(ErrorCode errorCode) {
this.errorCode = errorCode;
this.message = errorCode.getDescription();
this.httpStatus = errorCode.getHttpStatus();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,15 @@ public ResponseEntity<ErrorResponseDto> handleSocialLoginFailureException(
.status(e.getHttpStatus())
.body(responseDto);
}

@ExceptionHandler(CustomAuthException.class)
public ResponseEntity<ErrorResponseDto> handleCustomAuthException(CustomAuthException e) {
ErrorResponseDto responseDto = ErrorResponseDto.from(e.getErrorCode());
log.warn(e + "");
log.warn("CustomAuthenticationPrincipal 인증정보를 가져오지 못했습니다.");

return ResponseEntity
.status(e.getHttpStatus())
.body(responseDto);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.valuewith.tweaver.group.service;

import static com.valuewith.tweaver.constants.ErrorCode.GROUP_NOT_FOUND;
import static com.valuewith.tweaver.constants.ErrorCode.GROUP_NOT_FOUND_FOR_DELETE;

import com.valuewith.tweaver.alert.dto.AlertRequestDto;
import com.valuewith.tweaver.alert.entity.Alert;
import com.valuewith.tweaver.alert.repository.AlertRepository;
Expand All @@ -9,13 +12,13 @@
import com.valuewith.tweaver.defaultImage.entity.DefaultImage;
import com.valuewith.tweaver.defaultImage.repository.DefaultImageRepository;
import com.valuewith.tweaver.defaultImage.service.ImageService;
import com.valuewith.tweaver.exception.CustomException;
import com.valuewith.tweaver.group.dto.TripGroupRequestDto;
import com.valuewith.tweaver.group.entity.TripGroup;
import com.valuewith.tweaver.group.repository.TripGroupRepository;
import com.valuewith.tweaver.groupMember.entity.GroupMember;
import com.valuewith.tweaver.groupMember.repository.GroupMemberRepository;
import com.valuewith.tweaver.member.entity.Member;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -126,11 +129,16 @@ public List<TripGroup> findMyTripGroupListByMemberId(Long memberId) {

public Boolean checkLeader(Member member, Long tripGroupId) {
TripGroup foundTripGroup = tripGroupRepository.findById(tripGroupId)
.orElseThrow(() -> new RuntimeException("삭제하려는 그룹이 존재하지 않습니다."));
.orElseThrow(() -> new CustomException(GROUP_NOT_FOUND_FOR_DELETE));
return foundTripGroup.getMember().equals(member);
}

public List<TripGroup> findChatRoomByMemberId(Long memberId) {
return tripGroupRepository.findChatRoomByMemberId(memberId);
}

public TripGroup findTripByTripGroupId(Long tripGroupId) {
return tripGroupRepository.findById(tripGroupId)
.orElseThrow(() -> new CustomException(GROUP_NOT_FOUND));
}
}
Loading

0 comments on commit d182df0

Please sign in to comment.