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
Expand Up @@ -12,6 +12,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -21,10 +22,16 @@
@RestController
@RequestMapping("/api/auth/re-auth")
@RequiredArgsConstructor
@Validated
public class ReauthController {
private final ReauthService reauthService;

@Operation(summary = "비밀번호 재인증", description = "현재 비밀번호 검증 후 reauth_token 발급(TTL=5분)")
@Operation(summary = "비밀번호 재인증", description = "현재 비밀번호 검증 후 reauth_token 발급(TTL=10분)")
@io.swagger.v3.oas.annotations.responses.ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "재인증 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "비밀번호 불일치 / 재인증 실패"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "비밀번호 재인증을 사용할 수 없는 계정(소셜-only/LOCAL 미연동)")
})
@PostMapping("/password")
public ResponseEntity<ApiResponse<ReauthResponse>> reauthByPassword(
@AuthenticationPrincipal JwtUserPrincipal principal,
Expand All @@ -33,7 +40,12 @@ public ResponseEntity<ApiResponse<ReauthResponse>> reauthByPassword(
return ResponseEntity.ok(ApiResponse.onSuccess(res));
}

@Operation(summary = "카카오 재인증", description = "카카오 accessToken 검증 후 reauth_token 발급(TTL=5분)")
@Operation(summary = "카카오 재인증", description = "카카오 accessToken 검증 후 reauth_token 발급(TTL=10분)")
@io.swagger.v3.oas.annotations.responses.ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "재인증 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "토큰 무효"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "카카오 연동이 없는 계정")
})
@PostMapping("/kakao")
public ResponseEntity<ApiResponse<ReauthResponse>> reauthByKakao(
@AuthenticationPrincipal JwtUserPrincipal principal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,59 @@
import com.indayvidual.server.global.api.response.ApiResponse;
import com.indayvidual.server.global.config.security.JwtUserPrincipal;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "MyPage API", description = "마이페이지 기능 제공")
@RestController
@RequestMapping("/api/mypage")
@RequiredArgsConstructor
@SecurityRequirement(name = "bearerAuth")
@Validated
public class UserProfileController {

private final UserProfileService userProfileService;
private final ReauthService reauthService;

@Operation(
summary = "내 프로필 조회",
description = "액세스 토큰만으로 내 프로필을 반환합니다. 재인증 토큰은 필요하지 않습니다."
)
@io.swagger.v3.oas.annotations.responses.ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 실패(액세스 토큰 누락/무효)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 없음")
})
@GetMapping("/profile")
public ResponseEntity<ApiResponse<UserResponseDTO.Profile>> getMyProfile(
@RequestHeader("X-Reauth-Token") String reauthToken,
@AuthenticationPrincipal JwtUserPrincipal principal
) {
reauthService.assertReauthOrThrow(principal.userId(), reauthToken, false);
var profile = userProfileService.getMyProfile(principal.userId());
return ResponseEntity.ok(ApiResponse.onSuccess(profile));
}

@Operation(summary = "닉네임 변경", description = "재인증 토큰 검증 후 닉네임을 변경합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "변경 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "재인증 필요"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 없음")
})
@PatchMapping("/update_username")
public ResponseEntity<ApiResponse<String>> updateUsername(
@AuthenticationPrincipal JwtUserPrincipal principal,
@Parameter(name = "X-Reauth-Token", in = ParameterIn.HEADER, required = true,
description = "재인증 토큰", example = "reauth_base64url_token")
@RequestHeader("X-Reauth-Token") String reauthToken,
@RequestBody @Valid UserRequestDTO.UpdateUsername dto
) {
Expand All @@ -48,9 +70,19 @@ public ResponseEntity<ApiResponse<String>> updateUsername(
return ResponseEntity.ok(ApiResponse.onSuccess("닉네임이 변경되었습니다."));
}

@Operation(summary = "비밀번호 변경", description = "재인증 토큰 1회용 검증 후 비밀번호를 변경합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "변경 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "새 비밀번호가 현재와 동일"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "재인증 필요 또는 현재 비밀번호 불일치"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "비밀번호 기반 변경이 불가능한 계정(소셜-only/LOCAL 미연동)")
})
@PatchMapping("/update_password")
public ResponseEntity<ApiResponse<String>> updatePassword(
@AuthenticationPrincipal JwtUserPrincipal principal,
@Parameter(name = "X-Reauth-Token", in = ParameterIn.HEADER, required = true,
description = "재인증 토큰", example = "reauth_base64url_token")
@RequestHeader("X-Reauth-Token") String reauthToken,
@RequestBody @Valid UserRequestDTO.UpdatePassword dto
) {
Expand All @@ -59,9 +91,20 @@ public ResponseEntity<ApiResponse<String>> updatePassword(
return ResponseEntity.ok(ApiResponse.onSuccess("비밀번호가 변경되었습니다."));
}

@Operation(summary = "프로필 이미지 변경",
description = "재인증 토큰 검증 후 이미지를 업로드합니다. 허용 형식: JPEG/PNG/WEBP, 최대 5MB.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "변경 성공 (프리사인드 URL 반환)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 파일 혹은 허용되지 않은 형식"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "재인증 필요"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "413", description = "파일 용량 초과"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "502", description = "업로드 실패")
})
@PatchMapping(value = "/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<String>> updateProfileImage(
@AuthenticationPrincipal JwtUserPrincipal principal,
@Parameter(name = "X-Reauth-Token", in = ParameterIn.HEADER, required = true,
description = "재인증 토큰", example = "reauth_base64url_token")
@RequestHeader("X-Reauth-Token") String reauthToken,
@RequestPart("image") MultipartFile image
) {
Expand All @@ -71,9 +114,16 @@ public ResponseEntity<ApiResponse<String>> updateProfileImage(
}

@Operation(summary = "회원 탈퇴", description = "재인증 토큰(X-Reauth-Token) 필요. 기본은 소프트 삭제")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "탈퇴 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "재인증 필요"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 없음")
})
@DeleteMapping("/delete")
public ResponseEntity<ApiResponse<SimpleMessageResponse>> deleteMe(
@AuthenticationPrincipal JwtUserPrincipal principal,
@Parameter(name = "X-Reauth-Token", in = ParameterIn.HEADER, required = true,
description = "재인증 토큰", example = "reauth_base64url_token")
@RequestHeader("X-Reauth-Token") String reauthToken,
@RequestBody(required = false) DeleteAccountRequest req
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.indayvidual.server.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

@Getter
public class DeleteAccountRequest {
@Schema(description = "true면 하드 삭제, false면 소프트 삭제", example = "false")
private boolean hard; // true면 하드 삭제
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.indayvidual.server.domain.user.dto.request;


import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class ReauthKakaoRequest {
@NotBlank
@Schema(description = "카카오 액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9.KAKAO_ACCESS", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "kakaoAccessToken은 필수입니다.")
private String kakaoAccessToken;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.indayvidual.server.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class ReauthPasswordRequest {
@NotBlank
@Schema(description = "현재 비밀번호", example = "P@ssw0rd123!", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "currentPassword는 필수입니다.")
private String currentPassword;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.indayvidual.server.domain.user.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
Expand All @@ -8,16 +9,19 @@ public class UserRequestDTO {

@Getter
public static class UpdateUsername {
@NotBlank
@Size(min = 2, max = 20)
@Schema(description = "새 닉네임", example = "박감자", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "username은 필수입니다.")
@Size(min = 2, max = 20, message = "닉네임은 2~20자여야 합니다.")
private String username;
}

@Getter
public static class UpdatePassword {
@NotBlank
@Schema(description = "현재 비밀번호", example = "oldP@ssw0rd", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "currentPassword는 필수입니다.")
private String currentPassword;
@NotBlank
@Schema(description = "새 비밀번호(최소 8자)", example = "NewP@ssw0rd1", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "newPassword는 필수입니다.")
@Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
private String newPassword;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.indayvidual.server.domain.user.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ReauthResponse {
private String reauthToken; // UUID
private long expiresInSeconds; // 예: 300
@Schema(description = "재인증 토큰(Base64URL)", example = "dGhpcy1pcy1yZWF1dGgtdG9rZW4", requiredMode = Schema.RequiredMode.REQUIRED)
private String reauthToken;
@Schema(description = "만료까지 남은 초", example = "600", requiredMode = Schema.RequiredMode.REQUIRED)
private long expiresInSeconds;
}
Loading