Skip to content

Commit 2adb6c1

Browse files
authored
Merge pull request #313 from DevKor-github/codexd/swagger-coherent-openapi
[codex] Make Swagger match backend behavior
2 parents a333830 + 6fe018c commit 2adb6c1

7 files changed

Lines changed: 244 additions & 33 deletions

File tree

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,147 @@
11
package devkor.ontime_back.config;
22

33
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
4-
import io.swagger.v3.oas.annotations.enums.ParameterIn;
54
import io.swagger.v3.oas.annotations.servers.Server;
65
import io.swagger.v3.oas.models.Components;
76
import io.swagger.v3.oas.models.OpenAPI;
7+
import io.swagger.v3.oas.models.Operation;
8+
import io.swagger.v3.oas.models.PathItem;
89
import io.swagger.v3.oas.models.info.Info;
9-
import io.swagger.v3.oas.models.parameters.Parameter;
1010
import io.swagger.v3.oas.models.security.SecurityRequirement;
1111
import io.swagger.v3.oas.models.security.SecurityScheme;
12+
import org.springdoc.core.customizers.OpenApiCustomizer;
1213
import org.springframework.context.annotation.Bean;
1314
import org.springframework.context.annotation.Configuration;
1415

16+
import java.util.Collections;
17+
import java.util.List;
18+
import java.util.Set;
19+
1520
@Configuration
1621
@OpenAPIDefinition(
1722
servers = {
1823
@Server(url = "https://3.38.172.54.nip.io", description = "New Production Server"),
19-
@Server(url = "http://localhost:8080", description = "Local Serever")
24+
@Server(url = "http://localhost:8080", description = "Local Server")
2025
}
2126
)
2227
public class SwaggerConfig {
28+
private static final String ACCESS_TOKEN_SCHEME = "accessToken";
29+
private static final String REFRESH_TOKEN_SCHEME = "refreshToken";
30+
31+
private static final Set<String> PUBLIC_PATHS = Set.of(
32+
"/",
33+
"/health",
34+
"/account-deletion",
35+
"/privacy-policy",
36+
"/sign-up",
37+
"/login",
38+
"/oauth2/google/login",
39+
"/oauth2/kakao/login",
40+
"/oauth2/apple/login",
41+
"/swagger-ui.html",
42+
"/error"
43+
);
44+
45+
private static final List<String> PUBLIC_PATH_PREFIXES = List.of(
46+
"/actuator/health",
47+
"/v3/api-docs",
48+
"/swagger-ui",
49+
"/swagger-resources",
50+
"/webjars",
51+
"/css",
52+
"/images",
53+
"/js",
54+
"/favicon.ico",
55+
"/h2-console"
56+
);
57+
2358
@Bean
2459
public OpenAPI openAPI() {
2560
return new OpenAPI()
2661
.components(new Components()
27-
.addSecuritySchemes("accessToken", new SecurityScheme()
28-
.name("Authorization") // 헤더 이름
62+
.addSecuritySchemes(ACCESS_TOKEN_SCHEME, new SecurityScheme()
63+
.name("Authorization")
2964
.type(SecurityScheme.Type.HTTP)
3065
.scheme("bearer")
3166
.bearerFormat("JWT")
67+
.description("Send as `Authorization: Bearer <access token>`.")
68+
)
69+
.addSecuritySchemes(REFRESH_TOKEN_SCHEME, new SecurityScheme()
70+
.name("Authorization-refresh")
71+
.type(SecurityScheme.Type.APIKEY)
72+
.in(SecurityScheme.In.HEADER)
73+
.description("Send as `Authorization-refresh: Bearer <refresh token>` to reissue an access token.")
3274
)
3375
)
34-
.addSecurityItem(new SecurityRequirement().addList("accessToken")) // 요청에 SecurityScheme 적용
76+
.addSecurityItem(new SecurityRequirement().addList(ACCESS_TOKEN_SCHEME))
3577
.info(apiInfo());
3678
}
3779

80+
@Bean
81+
public OpenApiCustomizer coherentOperationCustomizer() {
82+
return openApi -> {
83+
if (openApi.getPaths() == null) {
84+
return;
85+
}
86+
87+
openApi.getPaths().forEach((path, pathItem) -> {
88+
if (pathItem == null) {
89+
return;
90+
}
91+
92+
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
93+
if (httpMethod == PathItem.HttpMethod.GET) {
94+
operation.setRequestBody(null);
95+
}
96+
if (isPublicPath(path)) {
97+
operation.setSecurity(Collections.emptyList());
98+
}
99+
removeStaleResponseExamples(operation);
100+
});
101+
});
102+
};
103+
}
104+
38105
private Info apiInfo() {
39106
return new Info()
40107
.title("Ontime")
41-
.description("Ontime API 명세서\n\n\n\n [JWT 인증 과정]\n\n/sign-up, /login, /{userId}/additional-info\n\n위 세 url을 제외하고는 헤더에 엑세스 토큰을 담아 요청을 보내야 함.\n\n(형식: \"Authorization [엑세스 토큰]\")\n\n\n토큰이 유효하면 요청이 처리될 것이고, 토큰이 유효하지 않으면 실패메세지가 반환될 것임.\n\n\n 엑세스토큰 인증이 실패하면 동일한 url(사실 아무 url이나 상관 없음. 실제로 해당 url로 요청 보내기전에 필터가 가로채서 처리함)로 헤더에 리프레시토큰을 담아 요청을 보내면 리프레시토큰의 유효성에 따라 엑세스토큰이 ResponseBody 재발급 될 것임.\n\n(형식: \"Authorization-refresh [리프레시토큰]\")")
108+
.description("""
109+
Ontime API 명세서
110+
111+
[JWT 인증 과정]
112+
공개 엔드포인트(`/sign-up`, `/login`, `/oauth2/google/login`, `/oauth2/kakao/login`, `/oauth2/apple/login`, `/health`, `/account-deletion`, `/privacy-policy`)를 제외한 API는 access token이 필요합니다.
113+
114+
Access token 요청 형식: `Authorization: Bearer <access token>`
115+
116+
Refresh token으로 access token을 재발급할 때는 보호 API 호출 전에 `Authorization-refresh: Bearer <refresh token>` 헤더를 보냅니다. 재발급 성공 시 새 access token은 응답 헤더 `Authorization`으로 반환됩니다.
117+
118+
일반 로그인과 소셜 로그인, 회원가입 성공 시 access token은 `Authorization` 헤더로, refresh token은 `Authorization-refresh` 헤더로 반환됩니다.
119+
""")
42120
.version("1.0.0");
43121
}
122+
123+
private boolean isPublicPath(String path) {
124+
return PUBLIC_PATHS.contains(path)
125+
|| PUBLIC_PATH_PREFIXES.stream().anyMatch(path::startsWith);
126+
}
127+
128+
private void removeStaleResponseExamples(Operation operation) {
129+
if (operation.getResponses() == null) {
130+
return;
131+
}
132+
133+
operation.getResponses().values().forEach(apiResponse -> {
134+
if (apiResponse.getContent() == null) {
135+
return;
136+
}
137+
138+
apiResponse.getContent().values().forEach(mediaType -> {
139+
mediaType.setExample(null);
140+
mediaType.setExamples(null);
141+
if (mediaType.getSchema() != null) {
142+
mediaType.getSchema().setExample(null);
143+
}
144+
});
145+
});
146+
}
44147
}

ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import devkor.ontime_back.dto.FeedbackAddDto;
44
import devkor.ontime_back.dto.OAuthAppleRequestDto;
5-
import devkor.ontime_back.dto.OAuthGoogleUserDto;
5+
import devkor.ontime_back.dto.OAuthGoogleRequestDto;
66
import devkor.ontime_back.dto.OAuthKakaoUserDto;
77
import devkor.ontime_back.global.oauth.apple.AppleLoginService;
88
import devkor.ontime_back.global.oauth.google.GoogleLoginService;
99
import devkor.ontime_back.response.ApiResponseForm;
1010
import devkor.ontime_back.service.UserAuthService;
1111
import io.swagger.v3.oas.annotations.Operation;
12+
import io.swagger.v3.oas.annotations.headers.Header;
1213
import io.swagger.v3.oas.annotations.media.Content;
1314
import io.swagger.v3.oas.annotations.media.Schema;
1415
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -34,27 +35,30 @@ public class SocialAuthController {
3435
@Operation(
3536
summary = "구글 소셜 로그인/회원가입",
3637
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
37-
description = "구글 회원정보 데이터",
38+
description = "구글 identity token 데이터",
3839
required = true,
3940
content = @Content(
4041
schema = @Schema(
41-
type = "object",
42-
example = "{\n \"idToken\": \"eyJhbGxxxxxxx\" ,\n \"refreshToken\": \"\"}}"
42+
implementation = OAuthGoogleRequestDto.class,
43+
example = "{\n \"idToken\": \"eyJhbGxxxxxxx\",\n \"refreshToken\": \"google-refresh-token\"\n}"
4344
)
4445
)
4546
)
4647
)
4748
@ApiResponses(value = {
48-
@ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
49+
@ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
50+
@Header(name = "Authorization", description = "Bearer access token"),
51+
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
52+
}, content = @Content(
4953
mediaType = "application/json",
5054
schema = @Schema(
51-
example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}"
55+
example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"로그인에 성공하였습니다.\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": 10,\n \"note\": null,\n \"punctualityScore\": 100.0,\n \"role\": \"USER\"\n }\n}"
5256
)
5357
)),
5458
@ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)")))
5559
})
5660
@PostMapping("/google/login")
57-
public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuthGoogleUserDto, HttpServletResponse response) {
61+
public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleRequestDto oAuthGoogleRequestDto, HttpServletResponse response) {
5862
return "구글 로그인/회원가입 성공"; // 로그인 처리는 필터에서 적용
5963
}
6064

@@ -65,14 +69,17 @@ public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuth
6569
required = true,
6670
content = @Content(
6771
schema = @Schema(
68-
type = "object",
69-
example = "{\n \"id\": \"4803687123\", \n \"profile\": {\n \"nickname\": \"김철수\", \n \"thumbnail_image_url\": \"http://dfsklafj;ewoai.jpg\", \n \"profile_image_url\": \"http://dfsklafj;ewoai.jpg\", \n\"is_default_image\": false, \n \"is_default_nickname\": false\n }\n}"
72+
implementation = OAuthKakaoUserDto.class,
73+
example = "{\n \"id\": \"4803687123\",\n \"profile\": {\n \"nickname\": \"김철수\",\n \"thumbnailImageUrl\": \"https://example.com/thumb.jpg\",\n \"profile_image_url\": \"https://example.com/profile.jpg\",\n \"defaultImage\": false,\n \"defaultNickname\": false\n }\n}"
7074
)
7175
)
7276
)
7377
)
7478
@ApiResponses(value = {
75-
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
79+
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
80+
@Header(name = "Authorization", description = "Bearer access token"),
81+
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
82+
}, content = @Content(
7683
mediaType = "application/json",
7784
schema = @Schema(
7885
example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}"
@@ -92,17 +99,19 @@ public String kakaoRegisterOrLogin(@Valid @RequestBody OAuthKakaoUserDto oAuthKa
9299
required = true,
93100
content = @Content(
94101
schema = @Schema(
95-
type = "object",
102+
implementation = OAuthAppleRequestDto.class,
96103
example = "{\n \"idToken\": \".\",\n \"authCode\": \".\",\n \"fullName\": \"허진서\" }" )
97104
)
98105
)
99106
)
100107
@ApiResponses(value = {
101-
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
108+
@ApiResponse(responseCode = "200", description = "애플 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
109+
@Header(name = "Authorization", description = "Bearer access token"),
110+
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
111+
}, content = @Content(
102112
mediaType = "application/json",
103113
schema = @Schema(
104-
type = "object",
105-
example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"%s\",\n \"data\": { \"userId\": %d,\n \"email\": \"%s\",\n \"name\": \"%s\",\n \"spareTime\": \"%s\",\n \"note\": \"%s\",\n \"punctualityScore\": %f,\n \"role\": \"%s\" }\n }"
114+
example = "{\n \"status\": \"success\",\n \"code\": \"200\",\n \"message\": \"로그인에 성공하였습니다.\",\n \"data\": {\n \"userId\": 1,\n \"email\": \"user@example.com\",\n \"name\": \"junbeom\",\n \"spareTime\": 10,\n \"note\": null,\n \"punctualityScore\": 100.0,\n \"role\": \"USER\"\n }\n}"
106115
)
107116
)),
108117
@ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)")))

0 commit comments

Comments
 (0)