From e3bcc2b70c315d559a926987500788879106b0ad Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 12 Aug 2026 13:58:33 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EA=B7=BC=EB=A1=9C=EC=9E=90=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=20=EB=A7=81=ED=81=AC=20API=203=EA=B0=9C=EC=97=90=20/a?= =?UTF-8?q?pi/v1=20=EC=A0=91=EB=91=90=EC=82=AC=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkerLinkViewController·WorkerLinkDocumentController·WorkerResponseController가 다른 모든 컨트롤러와 달리 /api/v1 접두사 없이 매핑되어 있어, 클라이언트가 실제로 호출하는 /api/v1/public/worker-links/** 경로에서는 SecurityConfig의 permitAll 규칙이 매칭되지 않고 인증이 필요한 규칙으로 떨어졌다. 근로자가 링크로 안내를 조회하거나 서류를 제출하는 흐름 전체가 실제 배포 환경에서 항상 실패하는 상태였다. 세 컨트롤러 매핑과 SecurityConfig의 permitAll 매처에 /api/v1 접두사를 추가하고, 같은 버그를 잡아내지 못했던 통합 테스트의 경로도 실제 API와 일치시켰다. Fixes #148 --- .../server/common/config/SecurityConfig.java | 4 ++-- .../api/WorkerLinkDocumentController.java | 2 +- .../api/WorkerLinkViewController.java | 2 +- .../api/WorkerResponseController.java | 2 +- .../PostgreSqlRestrictedRoleHttpE2ETest.java | 12 +++++------ .../WorkerLinkSecurityIntegrationTest.java | 20 +++++++++---------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/fowoco/server/common/config/SecurityConfig.java b/src/main/java/com/fowoco/server/common/config/SecurityConfig.java index 4797307b..dbd4e708 100644 --- a/src/main/java/com/fowoco/server/common/config/SecurityConfig.java +++ b/src/main/java/com/fowoco/server/common/config/SecurityConfig.java @@ -109,8 +109,8 @@ public SecurityFilterChain applicationSecurityFilterChain( "/api/v1/auth/password-resets" ).permitAll() .requestMatchers("/error").permitAll() - .requestMatchers(HttpMethod.GET, "/public/worker-links/**").permitAll() - .requestMatchers(HttpMethod.POST, "/public/worker-links/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/public/worker-links/**").permitAll() + .requestMatchers(HttpMethod.POST, "/api/v1/public/worker-links/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/**") .hasAnyRole("ADMIN", "HR", "VIEWER") .requestMatchers(HttpMethod.HEAD, "/api/v1/**") diff --git a/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkDocumentController.java b/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkDocumentController.java index 18e305ff..dde6990a 100644 --- a/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkDocumentController.java +++ b/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkDocumentController.java @@ -58,7 +58,7 @@ public WorkerLinkDocumentController(WorkerLinkDocumentService workerLinkDocument @ApiResponse(responseCode = "429", description = "요청 과다") }) @PostMapping( - path = "/public/worker-links/{token}/documents", + path = "/api/v1/public/worker-links/{token}/documents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE ) diff --git a/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkViewController.java b/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkViewController.java index d90f8d17..d698558a 100644 --- a/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkViewController.java +++ b/src/main/java/com/fowoco/server/workerlink/api/WorkerLinkViewController.java @@ -21,7 +21,7 @@ @Tag(name = "Worker Link (Public)", description = "근로자 공개 안내·제출") @RestController -@RequestMapping("/public/worker-links/{token}") +@RequestMapping("/api/v1/public/worker-links/{token}") public class WorkerLinkViewController { private final WorkerLinkViewService workerLinkViewService; diff --git a/src/main/java/com/fowoco/server/workerlink/api/WorkerResponseController.java b/src/main/java/com/fowoco/server/workerlink/api/WorkerResponseController.java index b5572793..23c05fa2 100644 --- a/src/main/java/com/fowoco/server/workerlink/api/WorkerResponseController.java +++ b/src/main/java/com/fowoco/server/workerlink/api/WorkerResponseController.java @@ -53,7 +53,7 @@ public WorkerResponseController(WorkerResponseService workerResponseService) { @ApiResponse(responseCode = "429", description = "요청 과다") }) @PostMapping( - path = "/public/worker-links/{token}/responses", + path = "/api/v1/public/worker-links/{token}/responses", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE ) diff --git a/src/test/java/com/fowoco/server/common/security/PostgreSqlRestrictedRoleHttpE2ETest.java b/src/test/java/com/fowoco/server/common/security/PostgreSqlRestrictedRoleHttpE2ETest.java index b5c77161..d9489444 100644 --- a/src/test/java/com/fowoco/server/common/security/PostgreSqlRestrictedRoleHttpE2ETest.java +++ b/src/test/java/com/fowoco/server/common/security/PostgreSqlRestrictedRoleHttpE2ETest.java @@ -36,7 +36,7 @@ class PostgreSqlRestrictedRoleHttpE2ETest { private static final String COOKIE_NAME = "fowoco_refresh_token"; - private static final String PROBE_PATH = "/public/worker-links/rls-test/probes"; + private static final String PROBE_PATH = "/api/v1/public/worker-links/rls-test/probes"; private static final List RLS_TABLES = List.of( "company", "user_account", @@ -259,7 +259,7 @@ void refreshRejectsUnknownExpiredAndRevokedTokensWithOneSafeContract() @Test void workerLinkBootstrapAcceptsOnlyActiveRegisteredLinks() throws Exception { HttpResponse active = get( - "/public/worker-links/" + "/api/v1/public/worker-links/" + PostgreSqlRestrictedRoleHttpDataFixture.ACTIVE_WORKER_LINK_TOKEN, null ); @@ -277,7 +277,7 @@ void workerLinkBootstrapAcceptsOnlyActiveRegisteredLinks() throws Exception { ); HttpResponse responseWrite = postJson( - "/public/worker-links/" + "/api/v1/public/worker-links/" + PostgreSqlRestrictedRoleHttpDataFixture.ACTIVE_WORKER_LINK_TOKEN + "/responses", """ @@ -298,16 +298,16 @@ void workerLinkBootstrapAcceptsOnlyActiveRegisteredLinks() throws Exception { ); assertThat(get( - "/public/worker-links/" + "/api/v1/public/worker-links/" + PostgreSqlRestrictedRoleHttpDataFixture.EXPIRED_WORKER_LINK_TOKEN, null ).statusCode()).isEqualTo(410); assertThat(get( - "/public/worker-links/" + "/api/v1/public/worker-links/" + PostgreSqlRestrictedRoleHttpDataFixture.REVOKED_WORKER_LINK_TOKEN, null ).statusCode()).isEqualTo(410); - assertThat(get("/public/worker-links/unregistered-rls-http-token", null).statusCode()) + assertThat(get("/api/v1/public/worker-links/unregistered-rls-http-token", null).statusCode()) .isEqualTo(410); } diff --git a/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java b/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java index ff715232..c700df37 100644 --- a/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java +++ b/src/test/java/com/fowoco/server/workerlink/WorkerLinkSecurityIntegrationTest.java @@ -130,7 +130,7 @@ void fullFlow_issueViewUploadRespond_succeeds() throws Exception { assertThat(rawToken).isNotBlank(); assertThat(workerUrl).isEqualTo("http://localhost:5173/worker-portal/" + rawToken); - HttpResponse viewResponse = getJson("/public/worker-links/" + rawToken, null); + HttpResponse viewResponse = getJson("/api/v1/public/worker-links/" + rawToken, null); assertThat(viewResponse.statusCode()).isEqualTo(200); assertThat(viewResponse.headers().firstValue("Cache-Control")).contains("no-store"); assertThat(JsonPath.read(viewResponse.body(), "$.guidance")) @@ -160,7 +160,7 @@ void fullFlow_issueViewUploadRespond_succeeds() throws Exception { assertThat(retryUploadId).isEqualTo(firstUploadId); HttpResponse responseSubmit = postJson( - "/public/worker-links/" + rawToken + "/responses", + "/api/v1/public/worker-links/" + rawToken + "/responses", """ {"response_type":"DOCUMENT_SUBMITTED","upload_ids":["%s"],"idempotency_key":"key-1"} """.formatted(uploadId), @@ -263,7 +263,7 @@ void activeWorkerLinkWithoutDraftReturnsContentNotReady() throws Exception { String taskId = createApprovedTask(hrToken, workerId); String workerUrl = issueWorkerLink(hrToken, taskId, "content-not-ready-key"); - HttpResponse response = getJson("/public/worker-links/" + workerUrl, null); + HttpResponse response = getJson("/api/v1/public/worker-links/" + workerUrl, null); assertThat(response.statusCode()).isEqualTo(409); assertThat(JsonPath.read(response.body(), "$.code")) @@ -278,7 +278,7 @@ void hrCanListAndMarkWorkerResponsesReviewed() throws Exception { String workerUrl = issueWorkerLink(hrToken, taskId, "response-management-key"); HttpResponse submitResponse = postJson( - "/public/worker-links/" + workerUrl + "/responses", + "/api/v1/public/worker-links/" + workerUrl + "/responses", """ {"response_type":"QUESTION","message":"여권의 어느 면을 제출하나요?","idempotency_key":"question-key"} """, @@ -360,7 +360,7 @@ void hrCanInspectAndAdoptSubmittedFilesThenResumeTask() throws Exception { String contractFileId = JsonPath.read(contractUpload.body(), "$.upload_id"); HttpResponse submitResponse = postJson( - "/public/worker-links/" + rawToken + "/responses", + "/api/v1/public/worker-links/" + rawToken + "/responses", """ { "response_type":"DOCUMENT_SUBMITTED", @@ -873,12 +873,12 @@ void issueRejectsOtherCompanyTask() throws Exception { @Test void viewReturns410ForNonExistentToken() throws Exception { - HttpResponse viewResponse = getJson("/public/worker-links/nonexistenttoken12345", null); + HttpResponse viewResponse = getJson("/api/v1/public/worker-links/nonexistenttoken12345", null); assertThat(viewResponse.statusCode()).isEqualTo(410); } @Test void documentsEndpointAllowsIdempotencyKeyHeaderInCors() throws Exception { - HttpRequest request = HttpRequest.newBuilder(uri("/public/worker-links/test-token/documents")) + HttpRequest request = HttpRequest.newBuilder(uri("/api/v1/public/worker-links/test-token/documents")) .header("Origin", "http://localhost:3000") .header("Access-Control-Request-Method", "POST") .header("Access-Control-Request-Headers", "Idempotency-Key") @@ -1008,7 +1008,7 @@ private HttpResponse uploadFile(String token, String filename, String mi writeFieldPart(out, "clientRequestId", UUID.randomUUID().toString()); out.write(("--" + BOUNDARY + "--\r\n").getBytes(StandardCharsets.UTF_8)); - HttpRequest request = HttpRequest.newBuilder(uri("/public/worker-links/" + token + "/documents")) + HttpRequest request = HttpRequest.newBuilder(uri("/api/v1/public/worker-links/" + token + "/documents")) .header(HttpHeaders.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY) .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray())) .build(); @@ -1028,7 +1028,7 @@ private HttpResponse uploadFileAsType( writeFieldPart(out, "documentType", documentType); out.write(("--" + BOUNDARY + "--\r\n").getBytes(StandardCharsets.UTF_8)); - HttpRequest request = HttpRequest.newBuilder(uri("/public/worker-links/" + token + "/documents")) + HttpRequest request = HttpRequest.newBuilder(uri("/api/v1/public/worker-links/" + token + "/documents")) .header(HttpHeaders.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY) .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray())) .build(); @@ -1043,7 +1043,7 @@ private HttpResponse uploadFileWithFixedClientRequestId( writeFieldPart(out, "clientRequestId", clientRequestId); out.write(("--" + BOUNDARY + "--\r\n").getBytes(StandardCharsets.UTF_8)); - HttpRequest request = HttpRequest.newBuilder(uri("/public/worker-links/" + token + "/documents")) + HttpRequest request = HttpRequest.newBuilder(uri("/api/v1/public/worker-links/" + token + "/documents")) .header(HttpHeaders.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY) .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray())) .build();