Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions src/main/java/com/example/feeda/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.example.feeda.config;

import com.example.feeda.filter.JwtFilter;
import com.example.feeda.security.handler.CustomAccessDeniedHandler;
import com.example.feeda.security.handler.CustomAuthenticationEntryPoint;
import com.example.feeda.security.jwt.JwtBlacklistService;
import com.example.feeda.security.jwt.JwtUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -22,6 +23,8 @@
public class SecurityConfig {
private final JwtBlacklistService jwtBlacklistService;
private final JwtUtil jwtUtil;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
Expand All @@ -43,10 +46,6 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
.requestMatchers("/error").permitAll()
.requestMatchers("/api/**").authenticated()

// 비로그인 시 GET 만 허용
// .requestMatchers(HttpMethod.GET, "/api/**").permitAll()
// .requestMatchers("/api/**").permitAll()

.anyRequest().denyAll()
)

Expand All @@ -55,20 +54,8 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti

.exceptionHandling(configurer ->
configurer
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");

String message = "{\"error\": \"인증 실패: " + authException.getMessage() + "\"}";
response.getWriter().write(message);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");

String message = "{\"error\": \"접근 거부: " + accessDeniedException.getMessage() + "\"}";
response.getWriter().write(message);
})
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)

.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.example.feeda.domain.account.controller;

import com.example.feeda.domain.account.sevice.AccountService;
import com.example.feeda.domain.account.sevice.AccountServiceImpl;
import com.example.feeda.domain.account.dto.*;
import com.example.feeda.security.jwt.JwtBlacklistService;
import com.example.feeda.security.jwt.JwtPayload;
import com.example.feeda.security.jwt.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
Expand All @@ -16,20 +17,20 @@
@RequestMapping("/api")
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;
private final AccountServiceImpl accountService;
private final JwtBlacklistService jwtBlacklistService;
private final JwtUtil jwtUtil;

@PostMapping("/accounts")
public ResponseEntity<UserResponseDTO> signup(@RequestBody SignUpRequestDTO requestDTO) {
public ResponseEntity<UserResponseDTO> signup(@RequestBody @Valid SignUpRequestDTO requestDTO) {
return new ResponseEntity<>(accountService.signup(requestDTO), HttpStatus.CREATED);
}

@DeleteMapping("/accounts/me")
public ResponseEntity<Void> deleteAccount(
@RequestHeader("Authorization") String bearerToken,
@AuthenticationPrincipal JwtPayload jwtPayload,
@RequestBody DeleteAccountRequestDTO requestDTO
@RequestBody @Valid DeleteAccountRequestDTO requestDTO
) {
accountService.deleteAccount(jwtPayload.getAccountId(), requestDTO.getPassword());

Expand All @@ -42,14 +43,14 @@ public ResponseEntity<Void> deleteAccount(
@PatchMapping("/accounts/password")
public ResponseEntity<UserResponseDTO> updatePassword(
@AuthenticationPrincipal JwtPayload jwtPayload,
@RequestBody UpdatePasswordRequestDTO requestDTO
@RequestBody @Valid UpdatePasswordRequestDTO requestDTO
) {
return new ResponseEntity<>(accountService.updatePassword(jwtPayload.getAccountId(), requestDTO), HttpStatus.OK);
}

@PostMapping("/accounts/login")
public ResponseEntity<UserResponseDTO> login(
@RequestBody LogInRequestDTO requestDTO
@RequestBody @Valid LogInRequestDTO requestDTO
) {
UserResponseDTO responseDTO = accountService.login(requestDTO);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,94 +1,16 @@
package com.example.feeda.domain.account.sevice;

import com.example.feeda.domain.account.dto.LogInRequestDTO;
import com.example.feeda.domain.account.dto.SignUpRequestDTO;
import com.example.feeda.domain.account.dto.UpdatePasswordRequestDTO;
import com.example.feeda.domain.account.dto.UserResponseDTO;
import com.example.feeda.domain.account.dto.SignUpRequestDTO;
import com.example.feeda.domain.account.entity.Account;
import com.example.feeda.domain.account.repository.AccountRepository;
import com.example.feeda.domain.profile.entity.Profile;
import com.example.feeda.domain.profile.repository.ProfileRepository;
import com.example.feeda.security.PasswordEncoder;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;



@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
private final PasswordEncoder passwordEncoder;
private final ProfileRepository profileRepository;

@Transactional
public UserResponseDTO signup(SignUpRequestDTO requestDTO) {
if(accountRepository.findByEmail(requestDTO.getEmail()).isPresent()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "이미 존재하는 이메일 입니다. : " + requestDTO.getEmail());
}

if(profileRepository.findByNickname(requestDTO.getNickName()).isPresent()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "이미 존재하는 닉네임 입니다. : " + requestDTO.getNickName());
}

Account account = new Account(requestDTO.getEmail(), requestDTO.getPassword());
account.setPassword(passwordEncoder.encode(account.getPassword()));

Profile profile = new Profile(requestDTO.getNickName(), requestDTO.getBirth(), requestDTO.getBio());

// 양방향 연결
account.setProfile(profile);
profile.setAccount(account);

Account saveProfile = accountRepository.save(account);

return new UserResponseDTO(saveProfile);
}

@Transactional
public void deleteAccount(Long id, String password) {
Account account = getAccountById(id);

if(!passwordEncoder.matches(password, account.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}

accountRepository.delete(account);
}

@Transactional
public UserResponseDTO updatePassword(Long id, UpdatePasswordRequestDTO requestDTO) {
Account account = getAccountById(id);

if(!passwordEncoder.matches(requestDTO.getOldPassword(), account.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}

account.setPassword(passwordEncoder.encode(requestDTO.getNewPassword()));

// DB 에 변경 사항 강제 반영
accountRepository.flush();

return new UserResponseDTO(account);
}


public UserResponseDTO login(LogInRequestDTO requestDTO) {
return new UserResponseDTO(accountRepository.findByEmail(requestDTO.getEmail())
.filter(findAccount -> passwordEncoder.matches(requestDTO.getPassword(), findAccount.getPassword()))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 일치하지 않습니다."))
);
}
public interface AccountService {
UserResponseDTO signup(SignUpRequestDTO requestDTO);

void deleteAccount(Long id, String password);

/* 유틸(?): 서비스 내에서만 사용 */
UserResponseDTO updatePassword(Long id, UpdatePasswordRequestDTO requestDTO);

public Account getAccountById(Long id) {
return accountRepository.findById(id).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 id 의 유저가 존재하지 않습니다. : " + id)
);
}
UserResponseDTO login(LogInRequestDTO requestDTO);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.example.feeda.domain.account.sevice;

import com.example.feeda.domain.account.dto.LogInRequestDTO;
import com.example.feeda.domain.account.dto.UpdatePasswordRequestDTO;
import com.example.feeda.domain.account.dto.UserResponseDTO;
import com.example.feeda.domain.account.dto.SignUpRequestDTO;
import com.example.feeda.domain.account.entity.Account;
import com.example.feeda.domain.account.repository.AccountRepository;
import com.example.feeda.domain.profile.entity.Profile;
import com.example.feeda.domain.profile.repository.ProfileRepository;
import com.example.feeda.exception.CustomResponseException;
import com.example.feeda.exception.enums.ResponseError;
import com.example.feeda.security.PasswordEncoder;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {
private final AccountRepository accountRepository;
private final PasswordEncoder passwordEncoder;
private final ProfileRepository profileRepository;

@Override
@Transactional
public UserResponseDTO signup(SignUpRequestDTO requestDTO) {
if(accountRepository.findByEmail(requestDTO.getEmail()).isPresent()) {
throw new CustomResponseException(ResponseError.EMAIL_ALREADY_EXISTS);
}

if(profileRepository.findByNickname(requestDTO.getNickName()).isPresent()) {
throw new CustomResponseException(ResponseError.NICKNAME_ALREADY_EXISTS);
}

Account account = new Account(requestDTO.getEmail(), requestDTO.getPassword());
account.setPassword(passwordEncoder.encode(account.getPassword()));

Profile profile = new Profile(requestDTO.getNickName(), requestDTO.getBirth(), requestDTO.getBio());

// 양방향 연결
account.setProfile(profile);
profile.setAccount(account);

Account saveProfile = accountRepository.save(account);

return new UserResponseDTO(saveProfile);
}

@Override
@Transactional
public void deleteAccount(Long id, String password) {
Account account = getAccountById(id);

if(!passwordEncoder.matches(password, account.getPassword())) {
throw new CustomResponseException(ResponseError.INVALID_PASSWORD);
}

accountRepository.delete(account);
}

@Override
@Transactional
public UserResponseDTO updatePassword(Long id, UpdatePasswordRequestDTO requestDTO) {
Account account = getAccountById(id);

if(!passwordEncoder.matches(requestDTO.getOldPassword(), account.getPassword())) {
throw new CustomResponseException(ResponseError.INVALID_PASSWORD);
}

account.setPassword(passwordEncoder.encode(requestDTO.getNewPassword()));

// DB 에 변경 사항 강제 반영
accountRepository.flush();

return new UserResponseDTO(account);
}

@Override
public UserResponseDTO login(LogInRequestDTO requestDTO) {
return new UserResponseDTO(accountRepository.findByEmail(requestDTO.getEmail())
.filter(findAccount -> passwordEncoder.matches(requestDTO.getPassword(), findAccount.getPassword()))
.orElseThrow(() -> new CustomResponseException(ResponseError.INVALID_EMAIL_OR_PASSWORD))
);
}


/* 유틸(?): 서비스 내에서만 사용 */

public Account getAccountById(Long id) {
return accountRepository.findById(id).orElseThrow(() ->
new CustomResponseException(ResponseError.ACCOUNT_NOT_FOUND)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import com.example.feeda.domain.comment.repository.CommentRepository;
import com.example.feeda.domain.profile.entity.Profile;
import com.example.feeda.domain.profile.repository.ProfileRepository;
import com.example.feeda.exception.CustomResponseException;
import com.example.feeda.exception.enums.ResponseError;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.Optional;

Expand All @@ -25,15 +25,15 @@ public class CommentLikeService {
public LikeCommentResponseDTO likeComment(Long commentId, Long profileId) {
Optional<CommentLike> findCommentLike = commentLikeRepository.findByComment_IdAndProfile_Id(commentId, profileId);
if(findCommentLike.isPresent()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 좋아요한 댓글입니다. : " + commentId);
throw new CustomResponseException(ResponseError.ALREADY_LIKED_COMMENT);
}

Comment findComment = commentRepository.findById(commentId).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 id 의 게시글이 존재하지 않습니다. : " + commentId)
new CustomResponseException(ResponseError.COMMENT_NOT_FOUND)
);

Profile findProfile = profileRepository.findById(profileId).orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 id 의 유저가 존재하지 않습니다. : " + profileId)
new CustomResponseException(ResponseError.PROFILE_NOT_FOUND)
);

CommentLike commentLike = new CommentLike(findComment, findProfile);
Expand All @@ -45,7 +45,7 @@ public LikeCommentResponseDTO likeComment(Long commentId, Long profileId) {
public void unlikeComment(Long commentId, Long profileId) {
Optional<CommentLike> findCommentLikeOptional = commentLikeRepository.findByComment_IdAndProfile_Id(commentId, profileId);
if(findCommentLikeOptional.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "아직 좋아요하지 않은 댓글 입니다. : " + commentId);
throw new CustomResponseException(ResponseError.NOT_YET_LIKED_COMMENT);
}

CommentLike commentLike = findCommentLikeOptional.get();
Expand Down
Loading