diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 7b08c3f..c74e0db 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -15975,189 +15975,6 @@ "summary" : "닉네임 변경", "tags" : [ "사용자" ] } - }, - "/users/{userId}" : { - "delete" : { - "description" : "본인만 요청할 수 있습니다", - "operationId" : "deleteUser", - "parameters" : [ { - "description" : "사용자 ID", - "in" : "path", - "name" : "userId", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "API 버전. 생략하면 현재 버전(1). 지원하지 않는 값이면 400", - "in" : "header", - "name" : "X-API-Version", - "required" : false, - "schema" : { - "default" : "1", - "type" : "string" - } - } ], - "responses" : { - "200" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/ApiSuccess" - } - } - }, - "description" : "OK" - } - }, - "security" : [ { - "Bearer Authentication" : [ ] - } ], - "summary" : "회원 탈퇴 (구 경로)", - "tags" : [ "사용자" ] - } - }, - "/users/{userId}/deactivate" : { - "put" : { - "description" : "본인만 요청할 수 있습니다", - "operationId" : "deactivateUser", - "parameters" : [ { - "description" : "사용자 ID", - "in" : "path", - "name" : "userId", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "API 버전. 생략하면 현재 버전(1). 지원하지 않는 값이면 400", - "in" : "header", - "name" : "X-API-Version", - "required" : false, - "schema" : { - "default" : "1", - "type" : "string" - } - } ], - "responses" : { - "200" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/ApiSuccess" - } - } - }, - "description" : "OK" - } - }, - "security" : [ { - "Bearer Authentication" : [ ] - } ], - "summary" : "계정 비활성화 (구 경로)", - "tags" : [ "사용자" ] - } - }, - "/users/{userId}/location" : { - "put" : { - "description" : "본인만 변경할 수 있습니다. 신규 클라이언트는 /users/me/location 을 사용하세요", - "operationId" : "updateUserLocation", - "parameters" : [ { - "description" : "사용자 ID", - "in" : "path", - "name" : "userId", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "위도", - "in" : "query", - "name" : "latitude", - "required" : true, - "schema" : { - "format" : "double", - "type" : "number" - } - }, { - "description" : "경도", - "in" : "query", - "name" : "longitude", - "required" : true, - "schema" : { - "format" : "double", - "type" : "number" - } - }, { - "description" : "API 버전. 생략하면 현재 버전(1). 지원하지 않는 값이면 400", - "in" : "header", - "name" : "X-API-Version", - "required" : false, - "schema" : { - "default" : "1", - "type" : "string" - } - } ], - "responses" : { - "200" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/UserDto" - } - } - }, - "description" : "OK" - } - }, - "security" : [ { - "Bearer Authentication" : [ ] - } ], - "summary" : "위치 갱신 (구 경로)", - "tags" : [ "사용자" ] - } - }, - "/users/{userId}/profile-image" : { - "put" : { - "description" : "본인만 변경할 수 있습니다. 신규 클라이언트는 /users/me/profile-image 를 사용하세요", - "operationId" : "updateProfileImage", - "parameters" : [ { - "description" : "사용자 ID", - "in" : "path", - "name" : "userId", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "프로필 이미지 URL", - "in" : "query", - "name" : "profileImageUrl", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "API 버전. 생략하면 현재 버전(1). 지원하지 않는 값이면 400", - "in" : "header", - "name" : "X-API-Version", - "required" : false, - "schema" : { - "default" : "1", - "type" : "string" - } - } ], - "responses" : { - "200" : { - "description" : "OK" - } - }, - "security" : [ { - "Bearer Authentication" : [ ] - } ], - "summary" : "프로필 이미지 변경 (구 경로)", - "tags" : [ "사용자" ] - } } }, "security" : [ { diff --git a/src/main/java/com/carecode/domain/user/controller/UserController.java b/src/main/java/com/carecode/domain/user/controller/UserController.java index 08f4d6e..e146ec2 100644 --- a/src/main/java/com/carecode/domain/user/controller/UserController.java +++ b/src/main/java/com/carecode/domain/user/controller/UserController.java @@ -142,7 +142,6 @@ public ResponseEntity uploadMyProfileImage( return ResponseEntity.ok(userFacade.uploadProfileImage(selfDbId(), file)); } - @PutMapping("/{userId}/profile-image") @LogExecutionTime @Operation(summary = "프로필 이미지 변경 (구 경로)", description = "본인만 변경할 수 있습니다. 신규 클라이언트는 /users/me/profile-image 를 사용하세요") @@ -167,7 +166,6 @@ public ResponseEntity updateMyLocation( return ResponseEntity.ok(userFacade.updateUserLocation(selfDbId(), latitude, longitude)); } - @PutMapping("/{userId}/location") @LogExecutionTime @Operation(summary = "위치 갱신 (구 경로)", description = "본인만 변경할 수 있습니다. 신규 클라이언트는 /users/me/location 을 사용하세요") @@ -191,7 +189,6 @@ public ResponseEntity deactivateMe() { return ResponseEntity.ok(ApiSuccess.of("회원 탈퇴가 완료되었습니다.")); } - @PutMapping("/{userId}/deactivate") @LogExecutionTime @Operation(summary = "계정 비활성화 (구 경로)", description = "본인만 요청할 수 있습니다") @SecurityRequirement(name = "Bearer Authentication") @@ -211,7 +208,6 @@ public ResponseEntity deleteMe() { return ResponseEntity.ok(ApiSuccess.of("회원 탈퇴가 완료되었습니다. 데이터는 보존되며 필요시 복구 가능합니다.")); } - @DeleteMapping("/{userId}") @LogExecutionTime @Operation(summary = "회원 탈퇴 (구 경로)", description = "본인만 요청할 수 있습니다") @SecurityRequirement(name = "Bearer Authentication") diff --git a/src/test/java/com/carecode/integration/AccessControlContractTest.java b/src/test/java/com/carecode/integration/AccessControlContractTest.java index 1f97923..bf47cb7 100644 --- a/src/test/java/com/carecode/integration/AccessControlContractTest.java +++ b/src/test/java/com/carecode/integration/AccessControlContractTest.java @@ -18,6 +18,7 @@ import org.springframework.test.web.servlet.MvcResult; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @@ -181,7 +182,13 @@ void emailVerificationLinkIsPublic() throws Exception { @ValueSource(strings = { "/users/1/role", "/users/1/activate", - "/users/1/reactivate" + "/users/1/reactivate", + // 아래 셋은 권한 상승이 아니라 "경로에 남의 ID 를 넣을 수 있는 것처럼 보이던" + // 구 경로다(본인 확인은 들어가 있었다). 대체 경로는 /users/me/* 이고 + // 프런트는 그쪽만 쓴다. 이슈 #91 + "/users/1/location", + "/users/1/profile-image", + "/users/1/deactivate" }) @WithMockUser(username = "attacker@example.com", roles = "PARENT") void privilegedMappingsRemovedFromUserApi(String path) throws Exception { @@ -193,6 +200,21 @@ void privilegedMappingsRemovedFromUserApi(String path) throws Exception { .isIn(404, 405); } + /** + * 회원 탈퇴 구 경로도 사라졌다. + * + *

{@code DELETE /users/{userId}} 는 본인 확인이 들어가 있었지만, 경로에 남의 ID 를 넣을 + * 수 있는 것처럼 보였다. 탈퇴는 {@code DELETE /users/me} 하나로만 받는다. 이슈 #91 + */ + @Test + @DisplayName("DELETE /users/{userId} 구 경로는 사라졌다") + @WithMockUser(username = "member@example.com", roles = "PARENT") + void legacyAccountDeletionPathRemoved() throws Exception { + assertThat(mockMvc.perform(delete("/users/1")).andReturn().getResponse().getStatus()) + .as("구 경로는 매핑이 없어야 한다 (404/405)") + .isIn(404, 405); + } + /** * 공공데이터 동기화는 외부 API 한도를 태우고 DB 에 쓴다. 공개로 두면 누구나 실행할 수 있다. *