diff --git a/ping/src/main/java/com/b6122/ping/config/SecurityConfig.java b/ping/src/main/java/com/b6122/ping/config/SecurityConfig.java index 95fd638..fec85d3 100644 --- a/ping/src/main/java/com/b6122/ping/config/SecurityConfig.java +++ b/ping/src/main/java/com/b6122/ping/config/SecurityConfig.java @@ -14,6 +14,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.filter.CorsFilter; @Configuration @@ -45,11 +46,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .httpBasic((httpBasic) -> httpBasic.disable()) //Bearer 방식을 사용하기 위해 basic 인증 비활성화 .authorizeHttpRequests((authorize) -> authorize - .requestMatchers("/user/**").hasAnyRole("ADMIN", "MANAGER", "USER") + .requestMatchers("/oauth/jwt/**").permitAll() .requestMatchers("/admin/**").hasAnyRole("ADMIN") - .anyRequest().permitAll()); - - + .requestMatchers("/**").hasAnyRole("ADMIN", "USER") + .anyRequest().authenticated()); return http.build(); diff --git a/ping/src/main/java/com/b6122/ping/config/jwt/JwtAuthorizationFilter.java b/ping/src/main/java/com/b6122/ping/config/jwt/JwtAuthorizationFilter.java index 3cde19c..b7411d7 100644 --- a/ping/src/main/java/com/b6122/ping/config/jwt/JwtAuthorizationFilter.java +++ b/ping/src/main/java/com/b6122/ping/config/jwt/JwtAuthorizationFilter.java @@ -2,9 +2,14 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; import com.b6122.ping.auth.PrincipalDetails; import com.b6122.ping.domain.User; +import com.b6122.ping.dto.UserDto; import com.b6122.ping.repository.UserRepository; +import com.b6122.ping.repository.datajpa.UserDataRepository; +import com.b6122.ping.service.JwtService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -16,10 +21,13 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import java.io.IOException; +import java.util.Map; public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private UserRepository userRepository; + private UserDataRepository userDataRepository; + private JwtService jwtService; public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) { super(authenticationManager); @@ -38,9 +46,40 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } //토큰 검증(동일한 토큰인지, 만료시간은 안지났는지 com.auth0.jwt 라이브러리가 확인해줌) + //access token과 refresh token을 검증 + String username = null; String token = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, ""); - String username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token) - .getClaim("username").asString(); + String tokenType = JWT.decode(token).getClaim("token_type").asString(); + + if (tokenType.equals("access")) { + + try { + username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token).getClaim("username").asString(); + + } catch(JWTVerificationException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Access token is not valid. Please send refersh token."); + return; + } + } else if (tokenType.equals("refresh")) { + + try { + username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token).getClaim("username").asString(); + User user = userDataRepository.findByUsername(username).orElseThrow(RuntimeException::new); + UserDto userDto = new UserDto(user.getId(), user.getUsername()); + Map jwtAccessToken = jwtService.createJwtAccessToken(userDto); + + //refersh token이 유효하면 access token 새로 발급 + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("{\"access_token\": \"" + jwtAccessToken.get("access_token") + "\"}"); + return; + + } catch (JWTVerificationException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Refresh token is not valid. Please login again."); + return; + } + } if (username != null) { User user = userRepository.findByUsername(username); diff --git a/ping/src/main/java/com/b6122/ping/controller/RestApiController.java b/ping/src/main/java/com/b6122/ping/controller/RestApiController.java index 3b890ef..d2340eb 100644 --- a/ping/src/main/java/com/b6122/ping/controller/RestApiController.java +++ b/ping/src/main/java/com/b6122/ping/controller/RestApiController.java @@ -1,84 +1,173 @@ package com.b6122.ping.controller; +import com.b6122.ping.auth.PrincipalDetails; +import com.b6122.ping.domain.Friendship; import com.b6122.ping.dto.UserDto; -import com.b6122.ping.service.JwtService; -import com.b6122.ping.service.KakaoOAuthService; -import com.b6122.ping.service.UserService; -import com.b6122.ping.service.GoogleOAuthService; -import com.b6122.ping.service.NaverOAuthService; +import com.b6122.ping.dto.UserProfileDto; +import com.b6122.ping.service.*; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; + import java.io.IOException; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; @RestController @RequiredArgsConstructor public class RestApiController { - private final NaverOAuthService naverOAuthService; private final GoogleOAuthService googleOAuthService; - private final JwtService jwtService; private final UserService userService; - private final KakaoOAuthService oAuthService; + private final KakaoOAuthService kakaoOAuthService; + private final FriendshipService friendshipService; + + //프론트엔드로부터 authorization code 받고 -> 그 code로 카카오에 accesstoken 요청 + // 받아 온 access token으로 카카오 리소스 서버로부터 카카오 유저 정보 가져오기 + // 가져온 정보를 기반으로 회원가입 + // jwt accessToken을 리액트 서버에 return + @PostMapping("/oauth/jwt/{serverName}") + public ResponseEntity> oauthLogin(@PathVariable("serverName") String server, + @RequestBody Map request) throws IOException { + if ("kakao".equals(server)) { + String accessToken = kakaoOAuthService.getKakaoAccessToken(request.get("code").toString()); + Map userInfo = kakaoOAuthService.getKakaoUserInfo(accessToken); + UserDto userDto = userService.joinOAuthUser(userInfo); + + return ResponseEntity.ok().body(jwtService.createJwtAccessAndRefreshToken(userDto)); + } else if ("google".equals(server)) { + String accessToken = googleOAuthService.getGoogleAccessToken(request.get("code").toString()); + Map userInfo = googleOAuthService.getGoogleUserInfo(accessToken); + UserDto userDto = userService.joinOAuthUser(userInfo); + + return ResponseEntity.ok().body(jwtService.createJwtAccessAndRefreshToken(userDto)); + } else { + Map data = new HashMap<>(); + data.put("error", "Nothing matches to request"); + + return ResponseEntity.badRequest().body(data); + } + } - @PostMapping("/oauth/jwt/kakao") - public ResponseEntity> createJwt(@RequestBody Map request) throws IOException { - // 프론트엔드로부터 authorization code 받고 -> 그 code로 카카오에 accesstoken 요청 - String accessToken = oAuthService.getKakaoAccessToken(request.get("code").toString()); + /** + * @param file : 사용자가 업로드한 이미지 파일 (form data) + * @param nickname : 사용자가 설정한 고유 nickname + */ + @PostMapping("/profile") + public void setInitialProfile(@RequestParam("profileImg") MultipartFile file, + @RequestParam("nickname") String nickname, + Authentication authentication) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + Long userId = principalDetails.getUser().getId(); + userService.updateProfile(file, nickname, userId); - // 받아 온 access token으로 카카오 리소스 서버로부터 카카오 유저 정보 가져오기 - Map userInfo = oAuthService.getKakaoUserInfo(accessToken); + } + + //회원 탈퇴 + @DeleteMapping("/account") + public void deleteAccount(Authentication authentication) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + userService.deleteAccount(principalDetails.getUser().getId()); + } - // 가져온 정보를 기반으로 회원가입 - UserDto userDto = userService.joinOAuthUser(userInfo); + //사용자 정보(닉네임, 사진) 가져오기 + @GetMapping("/account") + public ResponseEntity> account(Authentication authentication) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + UserProfileDto userInfoDto = userService.getUserProfile(principalDetails.getUser().getId()); + Map data = new HashMap<>(); + data.put("userInfo", userInfoDto); - // jwt accessToken을 리액트 서버에 return - return ResponseEntity.ok().body(jwtService.createJwtAccessToken(userDto)); + return ResponseEntity.ok().body(data); } - @PostMapping("/oauth/jwt/google") - public ResponseEntity> createGoogleJwt(@RequestBody Map request) throws IOException { - // Frontend sends the authorization code to the server - String authorizationCode = request.get("code").toString(); + //회원정보 변경(일단 사진만, 닉네임까지 확장 가능) + @PostMapping("/account") + public void updateProfileImage(@RequestParam("profileImg") MultipartFile file, + Authentication authentication) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + userService.updateProfile(file, + principalDetails.getUser().getNickname(), + principalDetails.getUser().getId()); + } - // Exchange the authorization code for an access token from Google - String accessToken = googleOAuthService.getGoogleAccessToken(authorizationCode); + /** + * 친구삭제 + * + * @param request {"nickname" : "xxx"} + */ + @DeleteMapping("/friends") + public void deleteFriend(@RequestBody Map request, Authentication authentication) { + String friendNickname = request.get("nickname").toString(); + UserProfileDto findUserDto = userService.findUserByNickname(friendNickname); + Long friendId = findUserDto.getId(); - // Use the access token to fetch user information from Google - Map userInfo = googleOAuthService.getGoogleUserInfo(accessToken); + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + Long userId = principalDetails.getUser().getId(); - // Process the user information and perform user registration if needed - UserDto userDto = userService.joinOAuthUser(userInfo); + friendshipService.deleteFriend(friendId, userId); - // Return the JWT access token to the React server - return ResponseEntity.ok().body(jwtService.createJwtAccessToken(userDto)); } - @CrossOrigin - @PostMapping("/oauth/jwt/naver") - public ResponseEntity> createJwtNaver(@RequestBody Map request) throws IOException { - // Frontend sends the authorization code, use it to request Naver for an access token - String accessToken = naverOAuthService.getNaverAccessToken(request.get("code").toString()); - - // Use the obtained access token to fetch Naver user information from Naver resource server - Map userInfo = naverOAuthService.getNaverUserInfo(accessToken); - // Based on the retrieved information, perform user registration - UserDto userDto = userService.joinOAuthUser(userInfo); + /** + * 사용자의 nickname을 검색하여 찾기 + * + * @param nickname 쿼리 파라미터로 전달 + * @return 사용자 정보(UserProfileDto -> nickname, profileImg), 친구 여부 + */ + @GetMapping("/friends/search") + public ResponseEntity> searchUser(@RequestParam("nickname") String nickname, + Authentication authentication) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + Map data = new HashMap<>(); + try { + UserProfileDto result = userService.findUserByNickname(nickname); + Optional friendship = + friendshipService.findFriendByIds(principalDetails.getUser().getId(), result.getId()); + data.put("userInfo", result); + data.put("isFriendWithMe", friendship.isPresent()); + return ResponseEntity.ok().body(data); + } catch (EntityNotFoundException e) { + data.put("error", e.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(data); + } + } - // Return the JWT access token to the React server - return ResponseEntity.ok().body(jwtService.createJwtAccessToken(userDto)); + //친구 신청 + @PostMapping("/friends/search") + public void sendFriendRequest(Authentication authentication, + @RequestParam("id") Long toUserId) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + Long fromUserId = principalDetails.getUser().getId(); + //userId는 친구 신청 하는 유저, friendId는 친구 신청 받는 유저 + friendshipService.sendRequest(fromUserId, toUserId); } + //친구 요청 수락 + @PostMapping("/friends/pendinglist") + public void addFriend(Authentication authentication, @RequestParam("nickname") String nickname) { + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + Long toUserId = principalDetails.getUser().getId(); + UserProfileDto findUserDto = userService.findUserByNickname(nickname); + Long fromUserId = findUserDto.getId(); + + //toUserId -> 친구 요청을 받은 유저 + //fromUserId -> 친구 요청을 보낸 유저 + friendshipService.addFriend(toUserId, fromUserId); + + } } diff --git a/ping/src/main/java/com/b6122/ping/domain/Friendship.java b/ping/src/main/java/com/b6122/ping/domain/Friendship.java index 8d340d1..f9d7dcd 100644 --- a/ping/src/main/java/com/b6122/ping/domain/Friendship.java +++ b/ping/src/main/java/com/b6122/ping/domain/Friendship.java @@ -4,9 +4,11 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Friendship { @@ -25,12 +27,22 @@ public class Friendship { @Enumerated(EnumType.STRING) private FriendshipRequestStatus requestStatus; // PENDING, ACCEPTED, REJECTED - private boolean isFriend; + private boolean isFriend = false; + public void setIsFriend(boolean isFriend) { + this.isFriend = isFriend; + } + + public void setRequestStatus(FriendshipRequestStatus requestStatus) { + this.requestStatus = requestStatus; + } //친구 요청 시 메소드 - //fromUser와 toUser사이 정방향/역방향 레코드 모두 추가 -// public static Friendship createFriendship() { -// -// } + public static Friendship createFriendship(User fromUser, User toUser) { + Friendship friendship = new Friendship(); + friendship.fromUser = fromUser; + friendship.toUser = toUser; + friendship.requestStatus = FriendshipRequestStatus.PENDING; + return friendship; + } } diff --git a/ping/src/main/java/com/b6122/ping/domain/Post.java b/ping/src/main/java/com/b6122/ping/domain/Post.java index 864c3b4..7f2f977 100644 --- a/ping/src/main/java/com/b6122/ping/domain/Post.java +++ b/ping/src/main/java/com/b6122/ping/domain/Post.java @@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; - import java.util.ArrayList; import java.util.List; @@ -25,6 +24,7 @@ @Table(name = "post") @NoArgsConstructor public class Post extends TimeEntity{ + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "post_id") @@ -53,8 +53,6 @@ public class Post extends TimeEntity{ @OneToMany(mappedBy = "post") private List likes = new ArrayList<>(); - - //연관관계 매서드// public void setUser(User user) { user.addPost(this); //user의 posts list에 post(this) 추가 @@ -62,7 +60,6 @@ public void setUser(User user) { /* - //like 눌렀을때 public void pushLike(Long uid){ postRepository.createLike(this.pid,uid); postRepository.updateLikeCount(this.getLikeCount()+1, this.pid); diff --git a/ping/src/main/java/com/b6122/ping/domain/User.java b/ping/src/main/java/com/b6122/ping/domain/User.java index fbb10e7..943a13d 100644 --- a/ping/src/main/java/com/b6122/ping/domain/User.java +++ b/ping/src/main/java/com/b6122/ping/domain/User.java @@ -19,9 +19,14 @@ public class User { private Long id; @Column(unique = true) - private String nickname; // 사용자가 직접 입력하는 고유닉네임 지예님 + private String nickname; // 사용자가 직접 입력하는 고유닉네임 - private String username; + /** oauth2 연동 유저정보(username, providerId, provider) **/ + private String provider; //"google", "kakao", etc. + private String providerId; //google, kakao 등 사용자의 고유Id (ex: google의 'sub'값 등) + private String username; // provider + _ + providerId + + private String profileImagePath; @Enumerated(EnumType.STRING) private UserRole role; // ROLE_USER or ROLE_ADMIN @@ -29,11 +34,21 @@ public class User { @OneToMany(mappedBy = "user") private List posts = new ArrayList<>(); + @OneToMany(mappedBy = "fromUser", cascade = CascadeType.REMOVE) + private List sentFriendshipRequests = new ArrayList<>(); + + @OneToMany(mappedBy = "toUser", cascade = CascadeType.REMOVE) + private List receivedFriendshipRequests = new ArrayList<>(); + public void addPost(Post p) {//외부에서 post 생성시 posts list에 추가 this.posts.add(p); } - /** oauth2 연동 유저정보(username, providerId, provider) **/ - private String provider; //"google", "kakao", etc. - private String providerId; //google, kakao 등 사용자의 고유Id (ex: google의 'sub'값 등) + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public void setProfileImagePath(String path) { + this.profileImagePath = path; + } } diff --git a/ping/src/main/java/com/b6122/ping/domain/UserRole.java b/ping/src/main/java/com/b6122/ping/domain/UserRole.java index 97118f9..acbcf60 100644 --- a/ping/src/main/java/com/b6122/ping/domain/UserRole.java +++ b/ping/src/main/java/com/b6122/ping/domain/UserRole.java @@ -2,6 +2,5 @@ public enum UserRole { ROLE_USER, - ROLE_ADMIN; - + ROLE_ADMIN } diff --git a/ping/src/main/java/com/b6122/ping/dto/FriendDto.java b/ping/src/main/java/com/b6122/ping/dto/FriendDto.java new file mode 100644 index 0000000..83e1bab --- /dev/null +++ b/ping/src/main/java/com/b6122/ping/dto/FriendDto.java @@ -0,0 +1,18 @@ +package com.b6122.ping.dto; + +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Data +public class FriendDto { + + private Long id; + private byte[] imageBytes; + private String nickname; + + public FriendDto(Long id, byte[] imageBytes, String nickname) { + this.id = id; + this.imageBytes = imageBytes; + this.nickname = nickname; + } +} diff --git a/ping/src/main/java/com/b6122/ping/dto/UserDto.java b/ping/src/main/java/com/b6122/ping/dto/UserDto.java index fa00f7d..cc722b8 100644 --- a/ping/src/main/java/com/b6122/ping/dto/UserDto.java +++ b/ping/src/main/java/com/b6122/ping/dto/UserDto.java @@ -5,15 +5,10 @@ @Data public class UserDto { - Long id; - String provider; - String providerId; - UserRole role; - - public UserDto(Long id, String provider, String providerId, UserRole role) { + private Long id; + private String username; + public UserDto(Long id,String username) { this.id = id; - this.provider = provider; - this.providerId = providerId; - this.role = role; + this.username = username; } } diff --git a/ping/src/main/java/com/b6122/ping/dto/UserProfileDto.java b/ping/src/main/java/com/b6122/ping/dto/UserProfileDto.java new file mode 100644 index 0000000..b8e4c49 --- /dev/null +++ b/ping/src/main/java/com/b6122/ping/dto/UserProfileDto.java @@ -0,0 +1,17 @@ +package com.b6122.ping.dto; + +import lombok.Data; + +@Data +public class UserProfileDto { + + private String nickname; + private byte[] profileImg; + private Long id; + + public UserProfileDto(Long id, String nickname, byte[] profileImg) { + this.nickname = nickname; + this.profileImg = profileImg; + this.id = id; + } +} diff --git a/ping/src/main/java/com/b6122/ping/oauth/provider/GoogleUser.java b/ping/src/main/java/com/b6122/ping/oauth/provider/GoogleUser.java index 4d8266d..f732725 100644 --- a/ping/src/main/java/com/b6122/ping/oauth/provider/GoogleUser.java +++ b/ping/src/main/java/com/b6122/ping/oauth/provider/GoogleUser.java @@ -22,7 +22,7 @@ public String getProvider() { @Override public String getName() { - return (String)attribute.get("name"); + return (String)attribute.get("username"); } } diff --git a/ping/src/main/java/com/b6122/ping/oauth/provider/KakaoUser.java b/ping/src/main/java/com/b6122/ping/oauth/provider/KakaoUser.java index d0a97d6..0d422be 100644 --- a/ping/src/main/java/com/b6122/ping/oauth/provider/KakaoUser.java +++ b/ping/src/main/java/com/b6122/ping/oauth/provider/KakaoUser.java @@ -22,7 +22,7 @@ public String getProvider() { @Override public String getName() { - return (String)attribute.get("name"); + return (String)attribute.get("username"); } } diff --git a/ping/src/main/java/com/b6122/ping/oauth/provider/NaverUser.java b/ping/src/main/java/com/b6122/ping/oauth/provider/NaverUser.java index f33ca1d..6d04bce 100644 --- a/ping/src/main/java/com/b6122/ping/oauth/provider/NaverUser.java +++ b/ping/src/main/java/com/b6122/ping/oauth/provider/NaverUser.java @@ -22,7 +22,7 @@ public String getProvider() { @Override public String getName() { - return (String)attribute.get("name"); + return (String)attribute.get("username"); } } diff --git a/ping/src/main/java/com/b6122/ping/oauth/provider/OAuthProperties.java b/ping/src/main/java/com/b6122/ping/oauth/provider/OAuthProperties.java index cd85fa0..b9d6f7f 100644 --- a/ping/src/main/java/com/b6122/ping/oauth/provider/OAuthProperties.java +++ b/ping/src/main/java/com/b6122/ping/oauth/provider/OAuthProperties.java @@ -1,6 +1,4 @@ package com.b6122.ping.oauth.provider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import java.security.SecureRandom; import java.math.BigInteger; @@ -9,15 +7,8 @@ public interface OAuthProperties { String KAKAO_CLIENT_ID = "4294c81106f19588526f3a34cb2b4356"; String KAKAO_REDIRECT_URI = "http://localhost:3000/authkakao"; - String GOOGLE_CLIENT_ID = "182133073202-g6sdd3ih0rpdlnjc14akqa1uj23ndbvh.apps.googleusercontent.com"; String GOOGLE_REDIRECT_URI = "http://localhost:3000/authgoogle"; String GOOGLE_CLIENT_SECRET = "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"; - - String NAVER_CLIENT_ID = "HT4vaEYy1mRjQXVadpwE"; - String NAVER_CLIENT_SECRET = "cDBX96chK3"; - String NAVER_REDIRECT_URI = "http://localhost:3000/authnaver"; - String NAVER_STATE = new BigInteger(130, new SecureRandom()).toString(32); - } diff --git a/ping/src/main/java/com/b6122/ping/repository/PostRepository.java b/ping/src/main/java/com/b6122/ping/repository/PostRepository.java index b8c956d..5ecac7d 100644 --- a/ping/src/main/java/com/b6122/ping/repository/PostRepository.java +++ b/ping/src/main/java/com/b6122/ping/repository/PostRepository.java @@ -16,7 +16,6 @@ @Repository @RequiredArgsConstructor - public class PostRepository{ private final EntityManager em; @@ -35,6 +34,7 @@ public int updateViewCount(@Param("viewCount") int viewCount, @Param("id") Long return 0; } + @Modifying @Query("update Post p set p.likeCount = :likeCount where p.id =:pid") public int updateLikeCount(@Param("likeCount") int likeCount, @Param("id") Long pid){ diff --git a/ping/src/main/java/com/b6122/ping/repository/datajpa/FriendshipDataRepository.java b/ping/src/main/java/com/b6122/ping/repository/datajpa/FriendshipDataRepository.java new file mode 100644 index 0000000..068bcc4 --- /dev/null +++ b/ping/src/main/java/com/b6122/ping/repository/datajpa/FriendshipDataRepository.java @@ -0,0 +1,43 @@ +package com.b6122.ping.repository.datajpa; + +import com.b6122.ping.domain.Friendship; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface FriendshipDataRepository extends JpaRepository { + + //친구 목록 조회 + @Query("select f from Friendship f" + + " join fetch f.fromUser" + + " join fetch f.toUser" + + " where (f.fromUser.id = :userId or f.toUser.id = :userId) and f.isFriend = true") + List findFriendshipsById(@Param("userId") Long userId); + + //친구 단건 조회 + @Query("select f from Friendship f" + + " where f.isFriend = true" + + " and ((f.toUser.id =:friendId and f.fromUser.id = :userId)" + + " or (f.toUser.id = :userId and f.fromUser.id = :friendId))") + Optional findFriendshipByIds(@Param("friendId") Long friendId, + @Param("userId") Long userId); + + /** + * fromUser가 보낸 아직 대기 중인(PENDING) 친구 요청 + * @param toUserId 친구 요청 받은 사람 id + * @param fromUserId 친구 요청 보낸 사람 id + * @return + */ + @Query("select f from Friendship f" + + " where f.isFriend = false" + + " and f.requestStatus = com.b6122.ping.domain.FriendshipRequestStatus.PENDING" + + " and f.toUser.id = :toUserId and f.fromUser.id = :fromUserId ") + Optional findPendingFriendShip(@Param("toUserId") Long toUserId, + @Param("fromUserId") Long fromUserId); + + +} + diff --git a/ping/src/main/java/com/b6122/ping/repository/datajpa/UserDataRepository.java b/ping/src/main/java/com/b6122/ping/repository/datajpa/UserDataRepository.java index 34c78b1..95baebd 100644 --- a/ping/src/main/java/com/b6122/ping/repository/datajpa/UserDataRepository.java +++ b/ping/src/main/java/com/b6122/ping/repository/datajpa/UserDataRepository.java @@ -2,10 +2,10 @@ import com.b6122.ping.domain.User; import org.springframework.data.jpa.repository.JpaRepository; - -import javax.swing.text.html.Option; import java.util.Optional; public interface UserDataRepository extends JpaRepository { Optional findByUsername(String username); + + Optional findByNickname(String nickname); } diff --git a/ping/src/main/java/com/b6122/ping/service/FriendshipService.java b/ping/src/main/java/com/b6122/ping/service/FriendshipService.java new file mode 100644 index 0000000..ac5e116 --- /dev/null +++ b/ping/src/main/java/com/b6122/ping/service/FriendshipService.java @@ -0,0 +1,140 @@ +package com.b6122.ping.service; + +import com.b6122.ping.domain.Friendship; +import com.b6122.ping.domain.FriendshipRequestStatus; +import com.b6122.ping.domain.User; +import com.b6122.ping.dto.FriendDto; +import com.b6122.ping.dto.UserProfileDto; +import com.b6122.ping.repository.datajpa.FriendshipDataRepository; +import com.b6122.ping.repository.datajpa.UserDataRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class FriendshipService { + + private final FriendshipDataRepository friendshipDataRepository; + private final UserService userService; + private final UserDataRepository userDataRepository; + + /** + * 친구의 고유 nickname으로 사용자의 친구 목록에서 삭제 + * @param friendId 전달 받은 삭제할 친구의 id + * @param userId 요청한 사용자의 id + */ + @Transactional + public void deleteFriend(Long friendId, Long userId) { + Friendship findFriendship = friendshipDataRepository.findFriendshipByIds(friendId, userId).orElseThrow(RuntimeException::new); + friendshipDataRepository.delete(findFriendship); + } + + /** + * 사용자의 id로 친구 목록 반환 + * @param id 요청한 사용자의 id + * @return 친구 목록 (FriendDto 정보: nickname, profileImg) + */ + public List findFriendsById(Long id) { + + //fromUser, toUser 페치 조인해서 가져옴 + List friendshipList = friendshipDataRepository.findFriendshipsById(id); + if (friendshipList.isEmpty()) { + return Collections.emptyList(); + } + + List friendDtos = new ArrayList<>(); + for (Friendship friendship : friendshipList) { + User fromUser = friendship.getFromUser(); + User toUser = friendship.getToUser(); + byte[] imageBytes; + FriendDto friendDto; + + //사용자가 친구 요청을 했을 경우 친구 상대방은 toUser + if (fromUser.getId().equals(id)) { + imageBytes = getByteArrayOfImageByPath(toUser.getProfileImagePath()); + friendDto = new FriendDto(id, imageBytes, toUser.getNickname()); + //사용자가 친구 요청을 받았을 경우 친구 상대방은 fromUser + } else { + imageBytes = getByteArrayOfImageByPath(fromUser.getProfileImagePath()); + friendDto = new FriendDto(fromUser.getId(), imageBytes, fromUser.getNickname()); + } + friendDtos.add(friendDto); + } + return friendDtos; + } + + /** + * @param imagePath 서버의 이미지 저장 장소 경로 + * @return 이미지의 byte배열 + */ + public byte[] getByteArrayOfImageByPath(String imagePath) { + try { + Resource resource = new UrlResource(Path.of(imagePath).toUri()); + if (resource.exists() && resource.isReadable()) { + // InputStream을 사용하여 byte 배열로 변환 + try (InputStream inputStream = resource.getInputStream()) { + byte[] data = new byte[inputStream.available()]; + inputStream.read(data); + return data; + } + } else { + // 이미지를 찾을 수 없는 경우 예외 또는 다른 처리 방법을 선택 + throw new RuntimeException("Image not found"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + /** + * 친구 단건 조회(FriendShip 엔티티 가져오기, isFriend가 true인 경우만) + */ + public Optional findFriendByIds(Long userId, Long friendId) { + return friendshipDataRepository.findFriendshipByIds(userId, friendId); + } + + /** + * 친구 요청 보내기 + * @param fromUserId ->친구 요청 보낸 사람 + * @param toUserId -> 친구 요청 받은 사람 + */ + @Transactional + public void sendRequest(Long fromUserId, Long toUserId) { + + User fromUser = userDataRepository.findById(fromUserId).orElseThrow(RuntimeException::new); + User toUser = userDataRepository.findById(toUserId).orElseThrow(RuntimeException::new); + + Friendship friendship = Friendship.createFriendship(fromUser, toUser); + + Optional findFriendship = friendshipDataRepository.findPendingFriendShip(fromUserId, toUserId); + if (findFriendship.isEmpty()) { + friendshipDataRepository.save(friendship); + } + + } + + /** + * 친구 요청 수락 + * @param toUserId (친구 요청 받은 사람) + * @param fromUserId (친구 요청 보낸 사람) + */ + @Transactional + public void addFriend(Long toUserId, Long fromUserId) { + Friendship friendship = friendshipDataRepository.findPendingFriendShip(toUserId, fromUserId).orElseThrow(RuntimeException::new); + friendship.setRequestStatus(FriendshipRequestStatus.ACCEPTED); + friendship.setIsFriend(true); + } +} diff --git a/ping/src/main/java/com/b6122/ping/service/GoogleOAuthService.java b/ping/src/main/java/com/b6122/ping/service/GoogleOAuthService.java index 83559c5..f1940e1 100644 --- a/ping/src/main/java/com/b6122/ping/service/GoogleOAuthService.java +++ b/ping/src/main/java/com/b6122/ping/service/GoogleOAuthService.java @@ -9,6 +9,7 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; import java.util.Map; @Service @@ -67,7 +68,6 @@ public String getGoogleAccessToken(String authorizationCode) throws IOException public Map getGoogleUserInfo(String accessToken) throws IOException { String requestEndpoint = "https://www.googleapis.com/oauth2/v1/userinfo"; - URL url = new URL(requestEndpoint); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); @@ -82,7 +82,9 @@ public Map getGoogleUserInfo(String accessToken) throws IOExcept response.append(line); } ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.readValue(response.toString(), Map.class); + Map userInfoMap = objectMapper.readValue(response.toString(), Map.class); + userInfoMap.put("provider", "google"); + return userInfoMap; } } else { try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream(), "UTF-8"))) { diff --git a/ping/src/main/java/com/b6122/ping/service/JwtService.java b/ping/src/main/java/com/b6122/ping/service/JwtService.java index 29f6459..db32e00 100644 --- a/ping/src/main/java/com/b6122/ping/service/JwtService.java +++ b/ping/src/main/java/com/b6122/ping/service/JwtService.java @@ -21,19 +21,52 @@ @RequiredArgsConstructor public class JwtService { + private final UserDataRepository userDataRepository; + + /** + * 사용자 정보를 바탕으로 Jwt AccessToken 발급 + * @param userDto UserDto 정보: id, username + * @return + */ + public Map createJwtAccessAndRefreshToken(UserDto userDto) { + + //accessToken 생성 + String accessToken = JWT.create() + .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.ACCESS_TOKEN_EXPIRATION_TIME)) + .withClaim("id", userDto.getId()) + .withClaim("username", userDto.getUsername()) + .withClaim("token_type", "access") + .sign(Algorithm.HMAC512(JwtProperties.SECRET)); + + //refreshToken 생성 + String refreshToken = JWT.create() + .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.REFRESH_TOKEN_EXPIRATION_TIME)) + .withClaim("id", userDto.getId()) + .withClaim("username", userDto.getUsername()) + .withClaim("token_type", "refresh") + .sign(Algorithm.HMAC512(JwtProperties.SECRET)); + + //responseBody에 값 저장해서 return + Map responseBody = new HashMap<>(); + responseBody.put("access_token", accessToken); + responseBody.put("refresh_token", refreshToken); + + return responseBody; + } + public Map createJwtAccessToken(UserDto userDto) { //accessToken 생성 - String jwtToken = JWT.create() - .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME)) + String accessToken = JWT.create() + .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.ACCESS_TOKEN_EXPIRATION_TIME)) .withClaim("id", userDto.getId()) + .withClaim("username", userDto.getUsername()) + .withClaim("token_type", "access") .sign(Algorithm.HMAC512(JwtProperties.SECRET)); //responseBody에 값 저장해서 return Map responseBody = new HashMap<>(); - responseBody.put("access_token", jwtToken); - responseBody.put("token_prefix", JwtProperties.TOKEN_PREFIX); - responseBody.put("expires_in", String.valueOf(JwtProperties.EXPIRATION_TIME)); + responseBody.put("access_token", accessToken); return responseBody; } diff --git a/ping/src/main/java/com/b6122/ping/service/KakaoOAuthService.java b/ping/src/main/java/com/b6122/ping/service/KakaoOAuthService.java index ff42d9e..eb32850 100644 --- a/ping/src/main/java/com/b6122/ping/service/KakaoOAuthService.java +++ b/ping/src/main/java/com/b6122/ping/service/KakaoOAuthService.java @@ -96,7 +96,9 @@ public Map getKakaoUserInfo(String accessToken) throws IOExcepti response.append(line); } ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.readValue(response.toString(), Map.class); + Map userInfoMap = objectMapper.readValue(response.toString(), Map.class); + userInfoMap.put("provider", "kakao"); + return userInfoMap; } } else { try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream(), "UTF-8"))) { diff --git a/ping/src/main/java/com/b6122/ping/service/NaverOAuthService.java b/ping/src/main/java/com/b6122/ping/service/NaverOAuthService.java index fdb395b..cda5857 100644 --- a/ping/src/main/java/com/b6122/ping/service/NaverOAuthService.java +++ b/ping/src/main/java/com/b6122/ping/service/NaverOAuthService.java @@ -85,7 +85,9 @@ public Map getNaverUserInfo(String accessToken) throws IOExcepti response.append(line); } ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.readValue(response.toString(), Map.class); + Map userInfoMap = objectMapper.readValue(response.toString(), Map.class); + userInfoMap.put("provider", "naver"); + return userInfoMap; } } else { try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(connection.getErrorStream(), "UTF-8"))) { diff --git a/ping/src/main/java/com/b6122/ping/service/UserService.java b/ping/src/main/java/com/b6122/ping/service/UserService.java index 5369255..a880c97 100644 --- a/ping/src/main/java/com/b6122/ping/service/UserService.java +++ b/ping/src/main/java/com/b6122/ping/service/UserService.java @@ -2,23 +2,27 @@ import com.b6122.ping.domain.User; import com.b6122.ping.domain.UserRole; -import com.b6122.ping.dto.CreateJwtRequestDto; import com.b6122.ping.dto.UserDto; +import com.b6122.ping.dto.UserProfileDto; import com.b6122.ping.oauth.provider.GoogleUser; import com.b6122.ping.oauth.provider.KakaoUser; import com.b6122.ping.oauth.provider.NaverUser; import com.b6122.ping.oauth.provider.OAuthUser; import com.b6122.ping.repository.datajpa.UserDataRepository; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.*; @Service @RequiredArgsConstructor @@ -26,11 +30,21 @@ public class UserService { private final UserDataRepository userDataRepository; + //서버가 받은 이미지가 저장되는 로컬 디스크 주소 + @Value("${profile.image.upload-path}") + private String profileImagePath; + + /** + * 리소스 서버(kakao, google)로 부터 사용자 정보를 받은 후 그것을 바탕으로 회원가입 + * @param userInfoMap 사용자 정보 Map + * @return UserDto (id, username) + * @throws IOException + */ @Transactional public UserDto joinOAuthUser(Map userInfoMap) throws IOException { //OAuthUser 생성을 위한 매핑 - String provider = "kakao"; + String provider = userInfoMap.get("provider").toString(); String providerId = userInfoMap.get("id").toString(); String username = provider + "_" + providerId; @@ -45,19 +59,19 @@ public UserDto joinOAuthUser(Map userInfoMap) throws IOException //db에 회원 등록이 되어있는지 확인후, 안되어 있다면 회원가입 시도 User findUser = userDataRepository - .findByUsername(oAuthUser.getProvider() + "_" + oAuthUser.getProviderId()) + .findByUsername(oAuthUser.getName()) .orElseGet(() -> { User user = User.builder() .provider(oAuthUser.getProvider()) .providerId(oAuthUser.getProviderId()) + .username(oAuthUser.getName()) .role(UserRole.ROLE_USER) .build(); // 회원가입 return userDataRepository.save(user); }); - return new UserDto( findUser.getId(), findUser.getProvider(), - findUser.getProviderId(), findUser.getRole()); + return new UserDto(findUser.getId(), findUser.getUsername()); } @@ -75,4 +89,100 @@ protected OAuthUser createOAuthUser(String provider, Map userInf } } + /** + * 요청 사용자의 프로필 업데이트(변경감지) + * @param file 사용자가 업로드한 이미지 데이터 + * @param nickname 사용자가 설정한 닉네임 + * @param userId 사용자의 id + */ + @Transactional + public void updateProfile(MultipartFile file, String nickname, Long userId) { + try { + User user = userDataRepository.findById(userId).orElseThrow(RuntimeException::new); + user.setNickname(nickname); + user.setProfileImagePath(saveProfileImage(file)); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + /** + * 프로필 이미지를 서버 로컬 주소에 저장(배포 시 클라우드로 옮길 예정) + * @param file 사용자가 업로드한 이미지 파일 + * @return 저장 장소의 절대 경로(디렉토리 경로 + 이미지 파일의 이름) + * @throws IOException + */ + public String saveProfileImage(MultipartFile file) throws IOException { + + //이미지 경로의 중복 방지를 위해 랜덤값으로 파일 명 저장 + String imageName = UUID.randomUUID() + file.getOriginalFilename(); + String path = profileImagePath; + File fileDir = new File(profileImagePath); + + //지정한 디렉토리가 없으면 생성 + if (!fileDir.exists()) { + fileDir.mkdirs(); + } + + //새로운 파일로 변환해서 지정한 경로에 저장. + file.transferTo(new File(path, imageName)); + + //이미지 찾아올 때 파일 이름을 포함한 모든 경로가 필요하기 때문에 아래와 같이 저장. + path = profileImagePath + "\\" + imageName; + + return path; + } + + //계정 삭제 + @Transactional + public void deleteAccount(Long id) { + userDataRepository.deleteById(id); + } + + /** + * 사용자 정보(이미지, 닉네임) 가져오기 + * @param id 사용자의 id + * @return 사용자 정보(UserInfoDto 정보: nickname, profileImg) + */ + public UserProfileDto getUserProfile(Long id) { + User user = userDataRepository.findById(id).orElseThrow(RuntimeException::new); + String nickname = user.getNickname(); + byte[] imageBytes = getByteArrayOfImageByPath(user.getProfileImagePath()); + return new UserProfileDto(id, nickname, imageBytes); + } + + /** + * @param imagePath 서버의 이미지 저장 장소 경로 + * @return 이미지의 byte 배열 + */ + public byte[] getByteArrayOfImageByPath(String imagePath) { + try { + Resource resource = new UrlResource(Path.of(imagePath).toUri()); + if (resource.exists() && resource.isReadable()) { + // InputStream을 사용하여 byte 배열로 변환 + try (InputStream inputStream = resource.getInputStream()) { + byte[] data = new byte[inputStream.available()]; + inputStream.read(data); + return data; + } + } else { + // 이미지를 찾을 수 없는 경우 예외 또는 다른 처리 방법을 선택 + throw new RuntimeException("Image not found"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * nickname으로 유저 검색 + * @param nickname + * @return UserInfoDto(nickname, profileImg) + */ + public UserProfileDto findUserByNickname(String nickname) { + User findUser = userDataRepository.findByNickname(nickname).orElseThrow(EntityNotFoundException::new); + byte[] imageBytes = getByteArrayOfImageByPath(findUser.getProfileImagePath()); + return new UserProfileDto(findUser.getId(), findUser.getNickname(), imageBytes); + } }