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
117 changes: 110 additions & 7 deletions ontime-back/src/main/java/devkor/ontime_back/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -1,44 +1,147 @@
package devkor.ontime_back.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Collections;
import java.util.List;
import java.util.Set;

@Configuration
@OpenAPIDefinition(
servers = {
@Server(url = "https://3.38.172.54.nip.io", description = "New Production Server"),
@Server(url = "http://localhost:8080", description = "Local Serever")
@Server(url = "http://localhost:8080", description = "Local Server")
}
)
public class SwaggerConfig {
private static final String ACCESS_TOKEN_SCHEME = "accessToken";
private static final String REFRESH_TOKEN_SCHEME = "refreshToken";

private static final Set<String> PUBLIC_PATHS = Set.of(
"/",
"/health",
"/account-deletion",
"/privacy-policy",
"/sign-up",
"/login",
"/oauth2/google/login",
"/oauth2/kakao/login",
"/oauth2/apple/login",
"/swagger-ui.html",
"/error"
);

private static final List<String> PUBLIC_PATH_PREFIXES = List.of(
"/actuator/health",
"/v3/api-docs",
"/swagger-ui",
"/swagger-resources",
"/webjars",
"/css",
"/images",
"/js",
"/favicon.ico",
"/h2-console"
);

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("accessToken", new SecurityScheme()
.name("Authorization") // 헤더 이름
.addSecuritySchemes(ACCESS_TOKEN_SCHEME, new SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Send as `Authorization: Bearer <access token>`.")
)
.addSecuritySchemes(REFRESH_TOKEN_SCHEME, new SecurityScheme()
.name("Authorization-refresh")
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.description("Send as `Authorization-refresh: Bearer <refresh token>` to reissue an access token.")
)
)
.addSecurityItem(new SecurityRequirement().addList("accessToken")) // 요청에 SecurityScheme 적용
.addSecurityItem(new SecurityRequirement().addList(ACCESS_TOKEN_SCHEME))
.info(apiInfo());
}

@Bean
public OpenApiCustomizer coherentOperationCustomizer() {
return openApi -> {
if (openApi.getPaths() == null) {
return;
}

openApi.getPaths().forEach((path, pathItem) -> {
if (pathItem == null) {
return;
}

pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
if (httpMethod == PathItem.HttpMethod.GET) {
operation.setRequestBody(null);
}
if (isPublicPath(path)) {
operation.setSecurity(Collections.emptyList());
}
removeStaleResponseExamples(operation);
});
});
};
}

private Info apiInfo() {
return new Info()
.title("Ontime")
.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 [리프레시토큰]\")")
.description("""
Ontime API 명세서

[JWT 인증 과정]
공개 엔드포인트(`/sign-up`, `/login`, `/oauth2/google/login`, `/oauth2/kakao/login`, `/oauth2/apple/login`, `/health`, `/account-deletion`, `/privacy-policy`)를 제외한 API는 access token이 필요합니다.

Access token 요청 형식: `Authorization: Bearer <access token>`

Refresh token으로 access token을 재발급할 때는 보호 API 호출 전에 `Authorization-refresh: Bearer <refresh token>` 헤더를 보냅니다. 재발급 성공 시 새 access token은 응답 헤더 `Authorization`으로 반환됩니다.

일반 로그인과 소셜 로그인, 회원가입 성공 시 access token은 `Authorization` 헤더로, refresh token은 `Authorization-refresh` 헤더로 반환됩니다.
""")
.version("1.0.0");
}

private boolean isPublicPath(String path) {
return PUBLIC_PATHS.contains(path)
|| PUBLIC_PATH_PREFIXES.stream().anyMatch(path::startsWith);
}

private void removeStaleResponseExamples(Operation operation) {
if (operation.getResponses() == null) {
return;
}

operation.getResponses().values().forEach(apiResponse -> {
if (apiResponse.getContent() == null) {
return;
}

apiResponse.getContent().values().forEach(mediaType -> {
mediaType.setExample(null);
mediaType.setExamples(null);
if (mediaType.getSchema() != null) {
mediaType.getSchema().setExample(null);
}
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import devkor.ontime_back.dto.FeedbackAddDto;
import devkor.ontime_back.dto.OAuthAppleRequestDto;
import devkor.ontime_back.dto.OAuthGoogleUserDto;
import devkor.ontime_back.dto.OAuthGoogleRequestDto;
import devkor.ontime_back.dto.OAuthKakaoUserDto;
import devkor.ontime_back.global.oauth.apple.AppleLoginService;
import devkor.ontime_back.global.oauth.google.GoogleLoginService;
import devkor.ontime_back.response.ApiResponseForm;
import devkor.ontime_back.service.UserAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -34,27 +35,30 @@ public class SocialAuthController {
@Operation(
summary = "구글 소셜 로그인/회원가입",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "구글 회원정보 데이터",
description = "구글 identity token 데이터",
required = true,
content = @Content(
schema = @Schema(
type = "object",
example = "{\n \"idToken\": \"eyJhbGxxxxxxx\" ,\n \"refreshToken\": \"\"}}"
implementation = OAuthGoogleRequestDto.class,
example = "{\n \"idToken\": \"eyJhbGxxxxxxx\",\n \"refreshToken\": \"google-refresh-token\"\n}"
)
)
)
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
@ApiResponse(responseCode = "200", description = "구글 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
@Header(name = "Authorization", description = "Bearer access token"),
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
}, content = @Content(
mediaType = "application/json",
schema = @Schema(
example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}"
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}"
)
)),
@ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)")))
})
@PostMapping("/google/login")
public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuthGoogleUserDto, HttpServletResponse response) {
public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleRequestDto oAuthGoogleRequestDto, HttpServletResponse response) {
return "구글 로그인/회원가입 성공"; // 로그인 처리는 필터에서 적용
}

Expand All @@ -65,14 +69,17 @@ public String googleRegisterOrLogin(@Valid @RequestBody OAuthGoogleUserDto oAuth
required = true,
content = @Content(
schema = @Schema(
type = "object",
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}"
implementation = OAuthKakaoUserDto.class,
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}"
)
)
)
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
@Header(name = "Authorization", description = "Bearer access token"),
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
}, content = @Content(
mediaType = "application/json",
schema = @Schema(
example = "{\n \"message\": \"유저의 ROLE이 GUEST이므로 온보딩API를 호출해 온보딩을 진행해야합니다.\",\n \"role\": \"GUEST\"}"
Expand All @@ -92,17 +99,19 @@ public String kakaoRegisterOrLogin(@Valid @RequestBody OAuthKakaoUserDto oAuthKa
required = true,
content = @Content(
schema = @Schema(
type = "object",
implementation = OAuthAppleRequestDto.class,
example = "{\n \"idToken\": \".\",\n \"authCode\": \".\",\n \"fullName\": \"허진서\" }" )
)
)
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "카카오 로그인/회원가입 성공 (로그인시 data : login, 회원가입시 data : register", content = @Content(
@ApiResponse(responseCode = "200", description = "애플 로그인/회원가입 성공. 토큰은 응답 헤더에 반환됨", headers = {
@Header(name = "Authorization", description = "Bearer access token"),
@Header(name = "Authorization-refresh", description = "Bearer refresh token")
}, content = @Content(
mediaType = "application/json",
schema = @Schema(
type = "object",
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 }"
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}"
)
)),
@ApiResponse(responseCode = "4XX", description = "실패", content = @Content(mediaType = "application/json", schema = @Schema(example = "실패 메세지(이메일이 이미 존재할 경우, 이름이 이미 존재할 경우 다르게 출력)")))
Expand Down
Loading
Loading