Conversation
- springdoc 기반 OpenAPI 계약과 Swagger UI 경로를 추가 - /api/v1/** 보안 스키마와 엔드포인트 설명을 명시 - openapi.json 생성 태스크와 검증 테스트를 추가
📝 WalkthroughWalkthrough이 PR은 springdoc-openapi 의존성을 추가하고 OpenAPI 구성 및 Swagger 어노테이션을 도입하여 Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9dbe512ec0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| security = { | ||
| @SecurityRequirement(name = "bearerAuth"), | ||
| @SecurityRequirement(name = "accessTokenCookie") | ||
| } |
There was a problem hiding this comment.
Require refresh token cookie in logout OpenAPI security
The new contract marks POST /api/v1/auth/logout as authorized by bearerAuth or accessTokenCookie, but the implementation always calls CookieUtils.extractRefreshToken(request) and fails with INVALID_REFRESH_TOKEN when the refreshToken cookie is missing. This makes the published OpenAPI spec inaccurate and will cause generated clients/integration harnesses to send requests that are documented as valid but fail at runtime.
Useful? React with 👍 / 👎.
| @Operation( | ||
| summary = "Delete the authenticated user's account", | ||
| description = "Deletes the current account and clears authentication cookies.", | ||
| security = { |
There was a problem hiding this comment.
Document delete account endpoint as 204, not default 200
This endpoint returns ResponseEntity.noContent() (HTTP 204), but the added OpenAPI metadata does not declare a 204 response, so the generated snapshot currently advertises only 200 for DELETE /api/v1/users/me. Because this commit’s goal is machine-readable contract generation, the mismatch can break generated clients that validate expected status codes against the spec.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/main/resources/static/swagger-ui/index.html (1)
7-13: 버전 하드코딩으로 인한 유지보수 부담WebJars 버전(
5.29.4)이 HTML과build.gradle에 중복 선언되어 있습니다. 현재는 동기화되어 있지만, 향후 업그레이드 시 불일치 위험이 있습니다.springdoc의 기본 Swagger UI 페이지(
/swagger-ui/index.html자동 제공)를 사용하거나, 버전을 서버 사이드에서 주입하는 방식을 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/static/swagger-ui/index.html` around lines 7 - 13, 현재 index.html에 하드코딩된 WebJars 버전 문자열 "5.29.4"이 swagger-ui.css, swagger-ui-bundle.js, swagger-ui-standalone-preset.js 링크에 중복 선언되어 있어 유지보수 문제가 발생합니다; 해당 파일들에서 하드코딩된 "5.29.4"를 제거하고 대신 서버가 제공하는 기본 Swagger UI 페이지를 사용하거나(예: springdoc이 제공하는 /swagger-ui/index.html로 대체) 또는 템플릿 변수(예: ${swaggerUiVersion})로 버전을 주입해 build.gradle의 버전과 중앙 관리하도록 변경하세요; 대상 식별자로는 링크/스크립트 태그 내 "swagger-ui.css", "swagger-ui-bundle.js", "swagger-ui-standalone-preset.js"와 빌드 파일인 build.gradle을 참고해 수정하십시오.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@build.gradle`:
- Around line 30-31: Update the swagger-ui dependency version in build.gradle:
replace the existing implementation 'org.webjars:swagger-ui:5.29.4' with the
newer release 'org.webjars:swagger-ui:5.32.1' so the dependency coordinates
(org.webjars:swagger-ui) point to v5.32.1; keep the
springdoc-openapi-starter-webmvc-api dependency unchanged.
In `@docs/openapi/openapi.json`:
- Line 1: The OpenAPI spec documents deleteMyAccount as returning 200 but
UserController.deleteMyAccount() actually returns HTTP 204; update the
controller and/or spec so they match: add
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "204",
description = "No Content") to the UserController.deleteMyAccount(...) method
(alongside `@Operation`) and regenerate/update the OpenAPI definition so the
/api/v1/users/me DELETE operation uses response code 204 instead of 200.
- Line 1: The OpenAPI spec currently contains a default http://localhost server
because OpenApiConfig.gitRankerOpenApi() doesn't set servers; update
gitRankerOpenApi() to call the OpenAPI builder's .servers(...) and add Server
entries for production and development (or inject an environment property) so
the produced spec includes the correct HTTPS production URL (use Server class
and .url/.description in the builder chain).
In `@src/test/java/com/gitranker/api/docs/OpenApiDocsTest.java`:
- Around line 69-78: persistIfRequested currently calls
Files.createDirectories(outputFile.getParent()) but outputFile.getParent() can
be null when OPENAPI_OUTPUT_PROPERTY provides a filename with no parent (e.g.,
"openapi.json"), causing a NullPointerException; modify persistIfRequested to
check whether outputFile.getParent() is non-null before calling
Files.createDirectories (i.e., only create directories when
outputFile.getParent() != null), then proceed to Files.writeString(outputFile,
responseBody).
---
Nitpick comments:
In `@src/main/resources/static/swagger-ui/index.html`:
- Around line 7-13: 현재 index.html에 하드코딩된 WebJars 버전 문자열 "5.29.4"이
swagger-ui.css, swagger-ui-bundle.js, swagger-ui-standalone-preset.js 링크에 중복
선언되어 있어 유지보수 문제가 발생합니다; 해당 파일들에서 하드코딩된 "5.29.4"를 제거하고 대신 서버가 제공하는 기본 Swagger UI
페이지를 사용하거나(예: springdoc이 제공하는 /swagger-ui/index.html로 대체) 또는 템플릿 변수(예:
${swaggerUiVersion})로 버전을 주입해 build.gradle의 버전과 중앙 관리하도록 변경하세요; 대상 식별자로는 링크/스크립트
태그 내 "swagger-ui.css", "swagger-ui-bundle.js",
"swagger-ui-standalone-preset.js"와 빌드 파일인 build.gradle을 참고해 수정하십시오.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 18ccb1eb-f7f5-49d4-b672-6b59e11f3b1c
📒 Files selected for processing (13)
build.gradledocs/openapi/README.mddocs/openapi/openapi.jsonsrc/main/java/com/gitranker/api/domain/auth/AuthController.javasrc/main/java/com/gitranker/api/domain/badge/BadgeController.javasrc/main/java/com/gitranker/api/domain/ranking/RankingController.javasrc/main/java/com/gitranker/api/domain/user/UserController.javasrc/main/java/com/gitranker/api/global/config/OpenApiConfig.javasrc/main/java/com/gitranker/api/global/config/SecurityConfig.javasrc/main/resources/application.ymlsrc/main/resources/static/swagger-ui/index.htmlsrc/test/java/com/gitranker/api/docs/OpenApiDocsTest.javasrc/test/resources/application-openapi.yml
build.gradle
Outdated
| implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.16' | ||
| implementation 'org.webjars:swagger-ui:5.29.4' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
springdoc-openapi latest version 2025
💡 Result:
The latest version of springdoc-openapi is 3.0.2, released on 2026-02-28. This is the most recent stable release for the v3 branch, supporting Spring Boot 4.0.3 and Spring Framework 7. For Spring Boot 3.x, use the latest v2 release, which is 2.8.16 (released 2026-02-28).
Citations:
- 1: https://github.com/springdoc/springdoc-openapi/releases
- 2: https://github.com/springdoc/springdoc-openapi
- 3: https://springdoc.org/
🌐 Web query:
swagger-ui latest version 2026
💡 Result:
The latest version of Swagger UI is v5.32.1, released on 2026-03-17.
Citations:
- 1: https://github.com/swagger-api/swagger-ui/releases
- 2: https://github.com/swagger-api/swagger-ui/releases/tag/v5.32.1
swagger-ui 버전 업데이트 필요
springdoc-openapi-starter-webmvc-api:2.8.16은 최신 버전이 맞으나, swagger-ui:5.29.4는 최신 버전인 v5.32.1로 업데이트해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@build.gradle` around lines 30 - 31, Update the swagger-ui dependency version
in build.gradle: replace the existing implementation
'org.webjars:swagger-ui:5.29.4' with the newer release
'org.webjars:swagger-ui:5.32.1' so the dependency coordinates
(org.webjars:swagger-ui) point to v5.32.1; keep the
springdoc-openapi-starter-webmvc-api dependency unchanged.
docs/openapi/openapi.json
Outdated
| @@ -0,0 +1 @@ | |||
| {"openapi":"3.1.0","info":{"title":"Git Ranker API","description":"Machine-readable contract for Git Ranker's public `/api/v1/**` endpoints.\n\nAuthentication model:\n- Protected endpoints accept either an `Authorization: Bearer <JWT>` header or the `accessToken` cookie.\n- `/api/v1/auth/refresh` uses the `refreshToken` cookie.\n- Initial sign-in starts with the GitHub OAuth2 redirect flow exposed by Spring Security outside `/api/v1/**`.\n","version":"v1"},"servers":[{"url":"http://localhost","description":"Generated server url"}],"paths":{"/api/v1/users/{username}/refresh":{"post":{"tags":["Users"],"summary":"Refresh the authenticated user's score","description":"Recalculates the caller's own profile. The authenticated user must match the path username.","operationId":"refreshUser","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","pattern":"^(?=.{1,39}$)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseRegisterUserResponse"}}}}},"security":[{"bearerAuth":[]},{"accessTokenCookie":[]}]}},"/api/v1/auth/refresh":{"post":{"tags":["Auth"],"summary":"Refresh access and refresh tokens","description":"Requires the refreshToken cookie and rotates the active session tokens.","operationId":"refreshToken","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseVoid"}}}}},"security":[{"refreshTokenCookie":[]}]}},"/api/v1/auth/logout":{"post":{"tags":["Auth"],"summary":"Log out the current session","description":"Requires an authenticated session and the refreshToken cookie to invalidate the current login.","operationId":"logout","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseVoid"}}}}},"security":[{"bearerAuth":[]},{"accessTokenCookie":[]}]}},"/api/v1/auth/logout/all":{"post":{"tags":["Auth"],"summary":"Log out every session for the current user","description":"Revokes all refresh tokens for the authenticated user.","operationId":"logoutAll","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseVoid"}}}}},"security":[{"bearerAuth":[]},{"accessTokenCookie":[]}]}},"/api/v1/users/{username}":{"get":{"tags":["Users"],"summary":"Get a user's profile","description":"Returns the public Git Ranker profile for a GitHub username.","operationId":"getUser","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string","pattern":"^(?=.{1,39}$)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseRegisterUserResponse"}}}}}}},"/api/v1/ranking":{"get":{"tags":["Ranking"],"summary":"List ranking entries","description":"Returns paginated ranking results with an optional tier filter.","operationId":"getRankings","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0,"minimum":0}},{"name":"tier","in":"query","required":false,"schema":{"type":"string","enum":["CHALLENGER","MASTER","DIAMOND","EMERALD","PLATINUM","GOLD","SILVER","BRONZE","IRON"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseRankingList"}}}}}}},"/api/v1/badges/{tier}/badge":{"get":{"tags":["Badges"],"summary":"Render a tier badge","description":"Returns an SVG badge template for the requested tier.","operationId":"getBadgeByTier","parameters":[{"name":"tier","in":"path","required":true,"schema":{"type":"string","enum":["CHALLENGER","MASTER","DIAMOND","EMERALD","PLATINUM","GOLD","SILVER","BRONZE","IRON"]}}],"responses":{"200":{"description":"OK","content":{"image/svg+xml":{"schema":{"type":"string"}}}}}}},"/api/v1/badges/{nodeId}":{"get":{"tags":["Badges"],"summary":"Render a badge for a GitHub node id","description":"Returns an SVG badge for a user's current Git Ranker profile.","operationId":"getBadge","parameters":[{"name":"nodeId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"image/svg+xml":{"schema":{"type":"string"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["Auth"],"summary":"Get the current authenticated user","description":"Returns the current session user resolved from the access token.","operationId":"me","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApiResponseAuthMeResponse"}}}}},"security":[{"bearerAuth":[]},{"accessTokenCookie":[]}]}},"/api/v1/users/me":{"delete":{"tags":["Users"],"summary":"Delete the authenticated user's account","description":"Deletes the current account and clears authentication cookies.","operationId":"deleteMyAccount","responses":{"200":{"description":"OK"}},"security":[{"bearerAuth":[]},{"accessTokenCookie":[]}]}}},"components":{"schemas":{"ApiResponseRegisterUserResponse":{"type":"object","properties":{"result":{"type":"string","enum":["SUCCESS","ERROR"]},"data":{"$ref":"#/components/schemas/RegisterUserResponse"},"error":{"$ref":"#/components/schemas/ErrorMessage"}}},"ErrorMessage":{"type":"object","properties":{"type":{"type":"string"},"message":{"type":"string"},"data":{}}},"RegisterUserResponse":{"type":"object","properties":{"userId":{"type":"integer","format":"int64"},"githubId":{"type":"integer","format":"int64"},"nodeId":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"profileImage":{"type":"string"},"role":{"type":"string","enum":["GUEST","USER","ADMIN"]},"updatedAt":{"type":"string","format":"date-time"},"lastFullScanAt":{"type":"string","format":"date-time"},"totalScore":{"type":"integer","format":"int32"},"ranking":{"type":"integer","format":"int32"},"tier":{"type":"string","enum":["CHALLENGER","MASTER","DIAMOND","EMERALD","PLATINUM","GOLD","SILVER","BRONZE","IRON"]},"percentile":{"type":"number","format":"double"},"commitCount":{"type":"integer","format":"int32"},"issueCount":{"type":"integer","format":"int32"},"prCount":{"type":"integer","format":"int32"},"mergedPrCount":{"type":"integer","format":"int32"},"reviewCount":{"type":"integer","format":"int32"},"diffCommitCount":{"type":"integer","format":"int32"},"diffIssueCount":{"type":"integer","format":"int32"},"diffPrCount":{"type":"integer","format":"int32"},"diffMergedPrCount":{"type":"integer","format":"int32"},"diffReviewCount":{"type":"integer","format":"int32"},"isNewUser":{"type":"boolean"}}},"ApiResponseVoid":{"type":"object","properties":{"result":{"type":"string","enum":["SUCCESS","ERROR"]},"data":{},"error":{"$ref":"#/components/schemas/ErrorMessage"}}},"ApiResponseRankingList":{"type":"object","properties":{"result":{"type":"string","enum":["SUCCESS","ERROR"]},"data":{"$ref":"#/components/schemas/RankingList"},"error":{"$ref":"#/components/schemas/ErrorMessage"}}},"PageInfo":{"type":"object","properties":{"currentPage":{"type":"integer","format":"int32"},"pageSize":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"totalPages":{"type":"integer","format":"int32"},"isFirst":{"type":"boolean"},"isLast":{"type":"boolean"}}},"RankingList":{"type":"object","properties":{"rankings":{"type":"array","items":{"$ref":"#/components/schemas/UserInfo"}},"pageInfo":{"$ref":"#/components/schemas/PageInfo"}}},"UserInfo":{"type":"object","properties":{"username":{"type":"string"},"profileImage":{"type":"string"},"ranking":{"type":"integer","format":"int64"},"totalScore":{"type":"integer","format":"int32"},"tier":{"type":"string","enum":["CHALLENGER","MASTER","DIAMOND","EMERALD","PLATINUM","GOLD","SILVER","BRONZE","IRON"]}}},"ApiResponseAuthMeResponse":{"type":"object","properties":{"result":{"type":"string","enum":["SUCCESS","ERROR"]},"data":{"$ref":"#/components/schemas/AuthMeResponse"},"error":{"$ref":"#/components/schemas/ErrorMessage"}}},"AuthMeResponse":{"type":"object","properties":{"username":{"type":"string"},"profileImage":{"type":"string"},"role":{"type":"string","enum":["GUEST","USER","ADMIN"]}}}},"securitySchemes":{"bearerAuth":{"type":"http","description":"Send `Authorization: Bearer <JWT>` for protected API calls.","scheme":"bearer","bearerFormat":"JWT"},"accessTokenCookie":{"type":"apiKey","description":"Browser session alternative to bearerAuth.","name":"accessToken","in":"cookie"},"refreshTokenCookie":{"type":"apiKey","description":"Required by refresh and logout flows that rotate or revoke session tokens.","name":"refreshToken","in":"cookie"}}}} No newline at end of file | |||
There was a problem hiding this comment.
DELETE /api/v1/users/me 응답 코드가 실제 구현과 다릅니다.
OpenAPI 스펙에서 deleteMyAccount 응답이 "200":{"description":"OK"}로 문서화되어 있지만, UserController.deleteMyAccount()는 ResponseEntity.noContent().build()를 반환하여 실제로는 HTTP 204를 응답합니다.
컨트롤러에 @ApiResponse 어노테이션을 추가하여 올바른 응답 코드를 명시하세요:
`@Operation`(...)
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "204", description = "No Content")
public ResponseEntity<Void> deleteMyAccount(...) {🧰 Tools
🪛 Checkov (3.2.508)
[high] 1: Ensure that the global security field has rules defined
(CKV_OPENAPI_4)
[high] 1: Ensure that security operations is not empty.
(CKV_OPENAPI_5)
[high] 1: Ensure that API keys are not sent over cleartext
(CKV_OPENAPI_20)
[medium] 1: Ensure that arrays have a maximum number of items
(CKV_OPENAPI_21)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/openapi/openapi.json` at line 1, The OpenAPI spec documents
deleteMyAccount as returning 200 but UserController.deleteMyAccount() actually
returns HTTP 204; update the controller and/or spec so they match: add
`@io.swagger.v3.oas.annotations.responses.ApiResponse`(responseCode = "204",
description = "No Content") to the UserController.deleteMyAccount(...) method
(alongside `@Operation`) and regenerate/update the OpenAPI definition so the
/api/v1/users/me DELETE operation uses response code 204 instead of 200.
🧩 Analysis chain
🌐 Web query:
springdoc openapi server url configuration
💡 Result:
In springdoc-openapi, there is no direct application property like springdoc.openapi.servers.url to configure the OpenAPI servers URL (as of the latest properties docs updated 2024-09). By default, springdoc generates a server URL dynamically from the incoming HTTP request (e.g., current host/port/context-path), which appears in Swagger UI as "Generated server url". This can be incorrect behind proxies unless X-Forwarded-* headers are configured. To configure custom server URLs: 1. Use the @OpenAPIDefinition annotation on a configuration class or main application class: import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; @OpenAPIDefinition(servers = { @Server(url = "https://example.com/api", description = "Production"), @Server(url = "/", description = "Relative (default)") }) You can inject properties: @Server(url = "${swagger.url:http://localhost:8080}"). 2. Define a custom OpenAPI bean: import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.servers.Server; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI { return new OpenAPI .servers(java.util.List.of( new Server.url("https://example.com/api").description("Production"), new Server.url("http://localhost:8080").description("Development") )); } } Note: Avoid combining @OpenAPIDefinition with a custom OpenAPI bean, as it may conflict; remove the annotation if using the bean. 3. For fine-grained customization of the generated server (e.g., behind proxy), implement ServerBaseUrlCustomizer: import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpServletRequest; @Component public class CustomServerBaseUrl implements ServerBaseUrlCustomizer { @Override public String customize(HttpServletRequest request, String baseServerUrl) { // e.g., force HTTPS or adjust path return baseServerUrl.replace("http://", "https://"); } } 4. Recent development (post-2025): YAML properties like springdoc.open-api.servers[0].url may be supported in newer versions, but confirm in release notes. For proxy/reverse proxy issues, ensure server.forward-headers-strategy=native (Undertow) or equivalent, and proxy sends X-Forwarded-Proto, X-Forwarded-Host. These methods override the default generated server in /v3/api-docs and Swagger UI dropdown.
Citations:
- 1: https://springdoc.org/properties.html
- 2: Proposal: relative server url by default springdoc/springdoc-openapi#2499
- 3: Desired Server URLs springdoc/springdoc-openapi#2750
- 4: https://stackoverflow.com/questions/60625494/wrong-generated-server-url-in-springdoc-openapi-ui-swagger-ui-deployed-behin
- 5: https://springdoc.org/
- 6: How to change the "generated Servers" dropdown values springdoc/springdoc-openapi#89
- 7: https://springdoc.org/v1/properties.html
- 8: Customize Servers via application.yml springdoc/springdoc-openapi#2948
- 9: Add ServerBaseUrlCustomizer functionality to be able to customize the generated Server URL springdoc/springdoc-openapi#1543
🏁 Script executed:
# Find OpenAPI configuration files
find . -type f -name "*OpenApi*" -o -name "*Swagger*" | grep -E "\.(java|yml|yaml|properties)$" | head -20Repository: alexization/git-ranker
Length of output: 192
🏁 Script executed:
# Search for springdoc configuration in properties/yaml files
fd -e properties -e yml -e yaml | xargs grep -l "springdoc" 2>/dev/null | head -10Repository: alexization/git-ranker
Length of output: 100
🏁 Script executed:
# Search for server configuration in Java code
rg -t java "Server\(" --max-count 20Repository: alexization/git-ranker
Length of output: 48
🏁 Script executed:
cat -n src/main/java/com/gitranker/api/global/config/OpenApiConfig.javaRepository: alexization/git-ranker
Length of output: 2847
🏁 Script executed:
cat -n src/main/resources/application.yml | head -100Repository: alexization/git-ranker
Length of output: 1532
OpenApiConfig에서 프로덕션 서버 URL을 명시적으로 구성하세요.
현재 OpenApiConfig에는 서버 URL이 명시적으로 설정되지 않아, springdoc이 요청 컨텍스트에서 자동 생성한 http://localhost가 OpenAPI 스펙에 포함됩니다. 프로덕션 환경에서는 HTTPS 기반의 실제 API 서버 URL이 필요합니다.
OpenApiConfig.gitRankerOpenApi() 메서드의 OpenAPI 빌더 체인에 .servers() 메서드를 추가하여 환경별 서버 URL을 구성하세요:
.servers(List.of(
new Server().url("https://api.yourdomain.com").description("Production"),
new Server().url("http://localhost:8080").description("Development")
))또는 환경 속성을 주입하여 동적으로 구성할 수 있습니다.
🧰 Tools
🪛 Checkov (3.2.508)
[high] 1: Ensure that the global security field has rules defined
(CKV_OPENAPI_4)
[high] 1: Ensure that security operations is not empty.
(CKV_OPENAPI_5)
[high] 1: Ensure that API keys are not sent over cleartext
(CKV_OPENAPI_20)
[medium] 1: Ensure that arrays have a maximum number of items
(CKV_OPENAPI_21)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/openapi/openapi.json` at line 1, The OpenAPI spec currently contains a
default http://localhost server because OpenApiConfig.gitRankerOpenApi() doesn't
set servers; update gitRankerOpenApi() to call the OpenAPI builder's
.servers(...) and add Server entries for production and development (or inject
an environment property) so the produced spec includes the correct HTTPS
production URL (use Server class and .url/.description in the builder chain).
| private void persistIfRequested(String responseBody) throws IOException { | ||
| String outputPath = System.getProperty(OPENAPI_OUTPUT_PROPERTY); | ||
| if (outputPath == null || outputPath.isBlank()) { | ||
| return; | ||
| } | ||
|
|
||
| Path outputFile = Path.of(outputPath); | ||
| Files.createDirectories(outputFile.getParent()); | ||
| Files.writeString(outputFile, responseBody); | ||
| } |
There was a problem hiding this comment.
outputFile.getParent()가 null을 반환할 수 있습니다.
outputPath가 부모 디렉토리 없이 파일명만 지정된 경우(예: "openapi.json"), Path.of(outputPath).getParent()는 null을 반환하며 Files.createDirectories(null) 호출 시 NullPointerException이 발생합니다.
🛡️ null 체크 추가 제안
private void persistIfRequested(String responseBody) throws IOException {
String outputPath = System.getProperty(OPENAPI_OUTPUT_PROPERTY);
if (outputPath == null || outputPath.isBlank()) {
return;
}
Path outputFile = Path.of(outputPath);
- Files.createDirectories(outputFile.getParent());
+ Path parent = outputFile.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
Files.writeString(outputFile, responseBody);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private void persistIfRequested(String responseBody) throws IOException { | |
| String outputPath = System.getProperty(OPENAPI_OUTPUT_PROPERTY); | |
| if (outputPath == null || outputPath.isBlank()) { | |
| return; | |
| } | |
| Path outputFile = Path.of(outputPath); | |
| Files.createDirectories(outputFile.getParent()); | |
| Files.writeString(outputFile, responseBody); | |
| } | |
| private void persistIfRequested(String responseBody) throws IOException { | |
| String outputPath = System.getProperty(OPENAPI_OUTPUT_PROPERTY); | |
| if (outputPath == null || outputPath.isBlank()) { | |
| return; | |
| } | |
| Path outputFile = Path.of(outputPath); | |
| Path parent = outputFile.getParent(); | |
| if (parent != null) { | |
| Files.createDirectories(parent); | |
| } | |
| Files.writeString(outputFile, responseBody); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/java/com/gitranker/api/docs/OpenApiDocsTest.java` around lines 69 -
78, persistIfRequested currently calls
Files.createDirectories(outputFile.getParent()) but outputFile.getParent() can
be null when OPENAPI_OUTPUT_PROPERTY provides a filename with no parent (e.g.,
"openapi.json"), causing a NullPointerException; modify persistIfRequested to
check whether outputFile.getParent() is non-null before calling
Files.createDirectories (i.e., only create directories when
outputFile.getParent() != null), then proceed to Files.writeString(outputFile,
responseBody).
- 회원 탈퇴 204 응답과 서버 URL을 계약에 반영 - Swagger UI WebJar 버전을 빌드 변수로 중앙화 - OpenAPI 테스트와 스냅샷 재생성 검증을 보강
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d607b63e4f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| security = { | ||
| @SecurityRequirement(name = "bearerAuth"), | ||
| @SecurityRequirement(name = "accessTokenCookie") | ||
| } |
There was a problem hiding this comment.
Add refresh cookie to logout OpenAPI security requirements
/api/v1/auth/logout is documented with bearerAuth/accessTokenCookie only, but the controller always calls CookieUtils.extractRefreshToken(request) inside logout(...), so requests without a refreshToken cookie fail with INVALID_REFRESH_TOKEN. This makes the generated contract inaccurate and can break clients/harnesses that rely on OpenAPI to build valid logout requests; the operation should declare refreshTokenCookie as required alongside the authentication scheme.
Useful? React with 👍 / 👎.
* build: 백엔드 OpenAPI 계약 생성 기반 추가 - springdoc 기반 OpenAPI 계약과 Swagger UI 경로를 추가 - /api/v1/** 보안 스키마와 엔드포인트 설명을 명시 - openapi.json 생성 태스크와 검증 테스트를 추가 * fix: OpenAPI 계약 리뷰 피드백 반영 - 회원 탈퇴 204 응답과 서버 URL을 계약에 반영 - Swagger UI WebJar 버전을 빌드 변수로 중앙화 - OpenAPI 테스트와 스냅샷 재생성 검증을 보강
1) 요약
docs/openapi/openapi.json과 재생성 태스크를 고정해야 해서 지금 필요했습니다.2) 연관 이슈
3) 문제와 목표
/v3/api-docs,/swagger-ui/index.html,docs/openapi/openapi.json을 기준으로 공개/api/v1/**계약을 재생성하고 검토할 수 있습니다.4) 영향 범위
global/configdomain/authdomain/userdomain/rankingdomain/badgesrc/test/java/com/gitranker/api/docsdocs/openapi/api/v1/**OpenAPI 계약과 보안 스키마가 새로 생성됩니다./v3/api-docs,/swagger-ui/index.html이 추가됩니다.DELETE /api/v1/users/me가 실제 구현과 동일한204응답으로 문서화되고, 계약에 production/local 서버 URL이 명시됩니다./v3/api-docs,/swagger-ui/**,/webjars/**)를 permitAll로 추가했습니다.5) 검증 증거
./gradlew build -x test./gradlew test./gradlew integrationTestContainerFetchException)./gradlew test jacocoTestCoverageVerification./gradlew test --tests "com.gitranker.api.docs.OpenApiDocsTest"./gradlew generateOpenApiSpecjq '{servers: .servers, deleteMyAccountResponses: .paths["/api/v1/users/me"].delete.responses}' docs/openapi/openapi.json/v3/api-docs,/swagger-ui/index.html,docs/openapi/openapi.json,204응답, 서버 URL 확인)6) 관측성 확인
7) AI 리뷰 메모 (선택)
204응답, 서버 목록, Swagger UI 버전 중앙화)을 반영했고,OpenApiDocsTest의 parent null 가드는 현재 커밋에서 함께 반영했습니다.8) 리스크 및 롤백
build.gradle,SecurityConfig,OpenApiConfig,docs/openapi/, Swagger UI 정적 파일을 되돌리면 됩니다.9) 체크리스트