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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.sejongisc.backend.auth.controller;

import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -32,6 +33,7 @@ public class OauthLoginController {
private final JwtProvider jwtProvider;
private final OauthStateService oauthStateService;


@Value("${google.client.id}")
private String googleClientId;

Expand Down Expand Up @@ -171,4 +173,42 @@ public ResponseEntity<LoginResponse> OauthLogin(@PathVariable("provider") String
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(response);
}

@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
// 1️⃣ 헤더 유효성 검사
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest()
.body(Map.of("message", "잘못된 Authorization 헤더 형식입니다."));
}

String token = authorizationHeader.substring(7);

// 2️⃣ 예외 처리 및 멱등성 보장
try {
loginService.logout(token);
} catch (JwtException | IllegalArgumentException e) {
// 이미 만료되었거나 잘못된 토큰이라도 200 OK로 응답 (멱등성 보장)
log.warn("Invalid or expired JWT during logout: {}", e.getMessage());
} catch (Exception e) {
log.error("Unexpected error during logout", e);
// 내부 예외는 500으로 보내지 않고 안전하게 처리
}

// 3️⃣ Refresh Token 쿠키 삭제
ResponseCookie deleteCookie = ResponseCookie.from("refresh", "")
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(0)
.build();

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, deleteCookie.toString())
.body(Map.of("message", "로그아웃 성공"));
}


}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sejongisc.backend.auth.entity;

import jakarta.persistence. *;
import lombok.*;
import java.util.UUID;

@Entity
@Table(name = "refresh_token")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {

@Id
@Column(name = "user_id", nullable = false, columnDefinition = "uuid")
private UUID userid;

@Column(nullable = false, length = 500)
private String token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sejongisc.backend.auth.repository;

import org.sejongisc.backend.auth.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, UUID> {

Optional<RefreshToken> findByUserid(UUID userId);

void deleteByUserId(UUID userId);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.sejongisc.backend.auth.service;

import jakarta.servlet.http.HttpServletRequest;
import org.sejongisc.backend.auth.dto.LoginRequest;
import org.sejongisc.backend.auth.dto.LoginResponse;

public interface LoginService {
LoginResponse login(LoginRequest request);

void logout(String accessToken);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package org.sejongisc.backend.auth.service;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.auth.repository.RefreshTokenRepository;
import org.sejongisc.backend.common.auth.jwt.JwtParser;
import org.sejongisc.backend.common.auth.jwt.JwtProvider;
import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails;
import org.sejongisc.backend.common.exception.CustomException;
Expand All @@ -15,13 +19,19 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.sql.Ref;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtParser jwtParser;

@Override
@Transactional
Expand All @@ -46,4 +56,12 @@ public LoginResponse login(LoginRequest request) {
.point(user.getPoint())
.build();
}

@Override
@Transactional
public void logout(String accessToken) {
UUID userId = jwtParser.getUserIdFromToken(accessToken);
refreshTokenRepository.deleteByUserId(userId);
log.info("로그아웃 완료: userId={}", userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface Oauth2Service<TToken, TUserInfo> {

TToken getAccessToken(String code);
TUserInfo getUserInfo(String accessToken);


}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public enum ErrorCode {
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."),
DUPLICATE_PHONE(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."),
DUPLICATE_USER(HttpStatus.CONFLICT, "이미 가입된 사용자입니다."),
INVALID_INPUT(HttpStatus.BAD_REQUEST, "입력값이 올바르지 않습니다."),

// BETTING

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
import org.sejongisc.backend.auth.dto.SignupRequest;
import org.sejongisc.backend.auth.dto.SignupResponse;
import org.sejongisc.backend.user.dto.UserInfoResponse;
import org.sejongisc.backend.user.dto.UserUpdateRequest;
import org.sejongisc.backend.user.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.UUID;

@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
Expand Down Expand Up @@ -43,4 +47,24 @@ public ResponseEntity<UserInfoResponse> getUserInfo(@AuthenticationPrincipal Cus

return ResponseEntity.ok(response);
}

@PatchMapping("/{userId}")
public ResponseEntity<?> updateUser(
@PathVariable UUID userId,
@RequestBody @Valid UserUpdateRequest request,
@AuthenticationPrincipal CustomUserDetails authenticatedUser
) {
if(authenticatedUser == null){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "인증 정보가 필요합니다."));
}

// 본인 허용
if (!authenticatedUser.getUserId().equals(userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "본인의 정보만 수정할 수 있습니다."));
}

userService.updateUser(userId, request);
return ResponseEntity.ok("회원 정보가 수정되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.sejongisc.backend.user.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserUpdateRequest {
private String name;
private String phoneNumber;
private String password; // 변경 시에만 받기
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import org.sejongisc.backend.auth.dto.SignupRequest;
import org.sejongisc.backend.auth.dto.SignupResponse;
import org.sejongisc.backend.user.dto.UserUpdateRequest;
import org.sejongisc.backend.user.entity.User;
import org.sejongisc.backend.auth.oauth.OauthUserInfo;

import java.util.UUID;

public interface UserService {
SignupResponse signUp(SignupRequest dto);

User findOrCreateUser(OauthUserInfo oauthInfo);

void updateUser(UUID userId, UserUpdateRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.sejongisc.backend.user.service;

import jakarta.transaction.Transactional;

import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.common.exception.CustomException;
Expand All @@ -9,6 +10,7 @@
import org.sejongisc.backend.user.dao.UserRepository;
import org.sejongisc.backend.auth.dto.SignupRequest;
import org.sejongisc.backend.auth.dto.SignupResponse;
import org.sejongisc.backend.user.dto.UserUpdateRequest;
import org.sejongisc.backend.user.entity.Role;
import org.sejongisc.backend.user.entity.User;
import org.sejongisc.backend.auth.entity.UserOauthAccount;
Expand All @@ -17,6 +19,8 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
Expand Down Expand Up @@ -93,4 +97,32 @@ public User findOrCreateUser(OauthUserInfo oauthInfo) {
});
}

@Override
@Transactional
public void updateUser(UUID userId, UserUpdateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// 이름 업데이트
if (request.getName() != null && !request.getName().trim().isEmpty()) {
user.setName(request.getName().trim());
}

// 전화번호 업데이트
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
user.setPhoneNumber(request.getPhoneNumber().trim());
}

if (request.getPassword() != null) {
String trimmedPassword = request.getPassword().trim();
if (trimmedPassword.isEmpty()) {
throw new CustomException(ErrorCode.INVALID_INPUT);
}
user.setPasswordHash(passwordEncoder.encode(trimmedPassword));
}

log.info("회원 정보가 수정되었습니다. userId={}", userId);
userRepository.save(user);
}

}
Loading