diff --git a/BE/eeos/build.gradle b/BE/eeos/build.gradle index d5358b76..094f684f 100644 --- a/BE/eeos/build.gradle +++ b/BE/eeos/build.gradle @@ -1,16 +1,17 @@ -buildscript { + buildscript { ext { projectName = 'eeos' projectVersion = '1.0.1' springBootVersion = '2.7.5' dependencyManagementVersion = '1.0.15.RELEASE' spotlessVersion = '6.8.0' + + set('springCloudVersion', "2021.0.4") + jsonwebtokenVersion = '0.11.5' + asciidoctorVersion = '3.3.2' epagesRestDocsApiSpecVersion = '0.16.0' - epagesRestDocsApiSpecVersion = '0.16.0' - asciidoctorVersion = '3.3.2' swaggerUIVersion = '4.11.1' - swaggerGeneratorVersion ='2.18.2' } } plugins { @@ -19,10 +20,10 @@ plugins { id 'io.spring.dependency-management' version "${dependencyManagementVersion}" id "com.diffplug.spotless" version "${spotlessVersion}" - - id 'com.epages.restdocs-api-spec' version "${epagesRestDocsApiSpecVersion}" + // docs plugins id 'org.asciidoctor.jvm.convert' version "${asciidoctorVersion}" - id 'org.hidetake.swagger.generator' version "${swaggerGeneratorVersion}" + id 'com.epages.restdocs-api-spec' version "${epagesRestDocsApiSpecVersion}" + id 'org.hidetake.swagger.generator' version '2.18.2' } group = 'com.econovation' @@ -42,16 +43,15 @@ repositories { mavenCentral() } -repositories { - mavenCentral() -} - /** apply tasks */ -apply from: './integration-test.gradle' - -apply from: './tasks/install-git-hooks.gradle' apply from: './tasks/formatting-task.gradle' -apply from: './tasks/docs-task.gradle' +apply from: './tasks/install-git-hooks.gradle' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' @@ -59,6 +59,7 @@ dependencies { // test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' // lombok compileOnly 'org.projectlombok:lombok' @@ -68,8 +69,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'mysql:mysql-connector-java' - // apache commons - implementation 'org.apache.commons:commons-lang3:3.0' + // openfeign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + + // jwt + implementation "io.jsonwebtoken:jjwt-api:${jsonwebtokenVersion}" + implementation "io.jsonwebtoken:jjwt-impl:${jsonwebtokenVersion}" + implementation "io.jsonwebtoken:jjwt-jackson:${jsonwebtokenVersion}" // swagger & restdocs implementation 'org.springdoc:springdoc-openapi-ui:1.6.9' @@ -79,7 +85,6 @@ dependencies { } test { - dependsOn spotlessApply useJUnitPlatform() testLogging { diff --git a/BE/eeos/resources/local-develop-environment/mysql-init.d/01_create_table.sql b/BE/eeos/resources/local-develop-environment/mysql-init.d/01_create_table.sql index aa46bf8b..af8d8b74 100644 --- a/BE/eeos/resources/local-develop-environment/mysql-init.d/01_create_table.sql +++ b/BE/eeos/resources/local-develop-environment/mysql-init.d/01_create_table.sql @@ -13,12 +13,12 @@ create table program create table member ( - member_id bigint not null auto_increment, - created_date datetime not null, - is_deleted boolean not null, - updated_date datetime not null, - member_name varchar(255) not null, - member_generation BIGINT not null, + member_id bigint not null auto_increment, + created_date datetime not null, + is_deleted boolean not null, + updated_date datetime not null, + member_name varchar(255) not null, + member_oath_server_type varchar(255) not null, primary key (member_id) ) engine = InnoDB; @@ -35,7 +35,7 @@ create table attend ) engine = InnoDB; ALTER TABLE member - ADD INDEX idx_generation_name (member_generation, member_name); + ADD INDEX idx_name (member_name); ALTER TABLE attend ADD INDEX idx_program (attend_program_id); diff --git a/BE/eeos/resources/local-develop-environment/mysql-init.d/02_add.data.sql b/BE/eeos/resources/local-develop-environment/mysql-init.d/02_add.data.sql deleted file mode 100644 index 4a91a749..00000000 --- a/BE/eeos/resources/local-develop-environment/mysql-init.d/02_add.data.sql +++ /dev/null @@ -1,14 +0,0 @@ -use eeos; - -insert into member (created_date, is_deleted, updated_date, member_name, member_generation) -values - ('2023-10-16 16:40:00', false, '2023-10-16 16:40:00', '김수민', 20), - ('2023-10-16 16:41:00', false, '2023-10-16 16:41:00', '강바다', 21), - ('2023-10-16 16:42:00', false, '2023-10-16 16:42:00', '만두', 23), - ('2023-10-16 16:43:00', false, '2023-10-16 16:43:00', '바다', 29), - ('2023-10-16 16:44:00', false, '2023-10-16 16:44:00', '카리나', 19), - ('2023-10-16 16:45:00', false, '2023-10-16 16:45:00', '사장님', 12), - ('2023-10-16 16:46:00', false, '2023-10-16 16:46:00', '장현지', 13), - ('2023-10-16 16:47:00', false, '2023-10-16 16:47:00', '박준수', 19), - ('2023-10-16 16:48:00', false, '2023-10-16 16:48:00', '박지유', 19), - ('2023-10-16 16:49:00', false, '2023-10-16 16:49:00', '스티브', 20); \ No newline at end of file diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/dto/converter/AttendInfoConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/dto/converter/AttendInfoConverter.java index eed52fe3..55dcd15a 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/dto/converter/AttendInfoConverter.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/dto/converter/AttendInfoConverter.java @@ -11,7 +11,6 @@ public class AttendInfoConverter { public AttendInfoResponse from(final MemberEntity source, final AttendStatus attendStatus) { return AttendInfoResponse.builder() .memberId(source.getId()) - .generation(source.getGeneration()) .name(source.getName()) .attendStatus(attendStatus.getStatus()) .build(); @@ -20,7 +19,6 @@ public AttendInfoResponse from(final MemberEntity source, final AttendStatus att public AttendInfoResponse from(final MemberEntity source, final String status) { return AttendInfoResponse.builder() .memberId(source.getId()) - .generation(source.getGeneration()) .name(source.getName()) .attendStatus(status) .build(); diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendException.java index 3c5390e2..ecb0008b 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendException.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendException.java @@ -3,9 +3,18 @@ import com.blackcompany.eeos.common.exception.BusinessException; import org.springframework.http.HttpStatus; +/** 존재하지 않는 참석 정보일 때 발생하는 예외 */ public class NotFoundAttendException extends BusinessException { + private static final String FAIL_CODE = "2004"; + private final Long programId; - public NotFoundAttendException() { - super("존재하지 않는 참석 정보입니다.", HttpStatus.NOT_FOUND); + public NotFoundAttendException(Long programId) { + super(FAIL_CODE, HttpStatus.NOT_FOUND); + this.programId = programId; + } + + @Override + public String getMessage() { + return String.format("%s 프로그램에 대한 참여 정보가 없습니다.", programId); } } diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendStatusException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendStatusException.java new file mode 100644 index 00000000..e3c60a91 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundAttendStatusException.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.attend.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 존재하지 않는 참석 상태일 때 발생하는 예외 */ +public class NotFoundAttendStatusException extends BusinessException { + private static final String FAIL_CODE = "2000"; + private final String attendStatus; + + public NotFoundAttendStatusException(String attendStatus) { + super(FAIL_CODE, HttpStatus.NOT_FOUND); + this.attendStatus = attendStatus; + } + + @Override + public String getMessage() { + return String.format("%s 참석 상태는 존재하지 않습니다.", attendStatus); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundStatusException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundStatusException.java deleted file mode 100644 index f4bad995..00000000 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundStatusException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.blackcompany.eeos.attend.application.exception; - -import com.blackcompany.eeos.common.exception.BusinessException; -import org.springframework.http.HttpStatus; - -public class NotFoundStatusException extends BusinessException { - - public NotFoundStatusException() { - super("존재하지 않는 상태입니다.", HttpStatus.NOT_FOUND); - } -} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeAttendStatusException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeAttendStatusException.java new file mode 100644 index 00000000..90ea5ff4 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeAttendStatusException.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.attend.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 이전 참석상태가 일치하지 않을 때 발생하는 예외 */ +public class NotSameBeforeAttendStatusException extends BusinessException { + private static final String FAIL_CODE = "2001"; + private final Long memberId; + + public NotSameBeforeAttendStatusException(Long memberId) { + super(FAIL_CODE, HttpStatus.NOT_FOUND); + this.memberId = memberId; + } + + @Override + public String getMessage() { + return String.format("%s 회원의 이전 상태가 올바르지 않습니다.", memberId); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeStatusException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeStatusException.java deleted file mode 100644 index 54bbf25d..00000000 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotSameBeforeStatusException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.blackcompany.eeos.attend.application.exception; - -import com.blackcompany.eeos.common.exception.BusinessException; -import org.springframework.http.HttpStatus; - -public class NotSameBeforeStatusException extends BusinessException { - - public NotSameBeforeStatusException() { - super("이전 참석 상태 정보가 올바르지 않습니다.", HttpStatus.BAD_REQUEST); - } -} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/service/AttendService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/service/AttendService.java index 562c163f..c0724bfd 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/service/AttendService.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/service/AttendService.java @@ -4,6 +4,7 @@ import com.blackcompany.eeos.attend.application.dto.ChangeStatusRequest; import com.blackcompany.eeos.attend.application.dto.converter.AttendInfoConverter; import com.blackcompany.eeos.attend.application.exception.NotFoundAttendException; +import com.blackcompany.eeos.attend.application.exception.NotSameBeforeAttendStatusException; import com.blackcompany.eeos.attend.application.model.AttendModel; import com.blackcompany.eeos.attend.application.model.AttendStatus; import com.blackcompany.eeos.attend.application.model.converter.AttendEntityConverter; @@ -44,7 +45,7 @@ private AttendStatus getAttendStatus(final Long memberId, final Long programId) return attendRepository .findByProgramIdAndMemberId(programId, memberId) .map(AttendEntity::getStatus) - .orElseThrow(NotFoundAttendException::new); + .orElseThrow(() -> new NotFoundAttendException(programId)); } @Override @@ -71,7 +72,7 @@ public void changeStatus(final ChangeStatusRequest request, final Long programId attendRepository .findByProgramIdAndMemberId(programId, request.getMemberId()) .map(attendEntityConverter::from) - .orElseThrow(NotFoundAttendException::new); + .orElseThrow(() -> new NotSameBeforeAttendStatusException(request.getMemberId())); model.isSame(request.getBeforeAttendStatus()); diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthMemberModel.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthMemberModel.java new file mode 100644 index 00000000..a44c56f5 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthMemberModel.java @@ -0,0 +1,19 @@ +package com.blackcompany.eeos.auth.application.domain; + +import com.blackcompany.eeos.common.support.AbstractModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class OauthMemberModel implements AbstractModel { + private String oauthId; + private String name; + private OauthServerType oauthServerType; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthServerType.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthServerType.java new file mode 100644 index 00000000..f44ec19a --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/OauthServerType.java @@ -0,0 +1,30 @@ +package com.blackcompany.eeos.auth.application.domain; + +import com.blackcompany.eeos.auth.application.exception.NotFoundOauthServerException; +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum OauthServerType { + SLACK("slack"); + + private final String oauthServer; + + OauthServerType(String oauthServer) { + this.oauthServer = oauthServer; + } + + public static OauthServerType find(String type) { + return Arrays.stream(values()) + .filter(oauthServerType -> oauthServerType.oauthServer.equals(type)) + .findAny() + .orElseThrow(() -> new NotFoundOauthServerException(type)); + } + + public static OauthServerType find(OauthServerType type) { + return Arrays.stream(values()) + .filter(oauthServerType -> oauthServerType.equals(type)) + .findAny() + .orElseThrow(() -> new NotFoundOauthServerException(type.getOauthServer())); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/TokenModel.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/TokenModel.java new file mode 100644 index 00000000..e239ef12 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/TokenModel.java @@ -0,0 +1,18 @@ +package com.blackcompany.eeos.auth.application.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class TokenModel { + + private String accessToken; + private Long accessExpiredTime; + private String refreshToken; + private Long refreshExpiredTime; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClient.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClient.java new file mode 100644 index 00000000..69c24979 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClient.java @@ -0,0 +1,10 @@ +package com.blackcompany.eeos.auth.application.domain.client; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; + +public interface OauthMemberClient { + OauthServerType support(); + + OauthMemberModel fetch(String code); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientComposite.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientComposite.java new file mode 100644 index 00000000..27ab7ec7 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientComposite.java @@ -0,0 +1,30 @@ +package com.blackcompany.eeos.auth.application.domain.client; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class OauthMemberClientComposite { + + private final Map clients; + + public OauthMemberClientComposite(Set providers) { + this.clients = + providers.stream() + .collect(Collectors.toMap(OauthMemberClient::support, Function.identity())); + } + + public OauthMemberModel fetch(String oauthServerType, String authCode) { + return getClient(oauthServerType).fetch(authCode); + } + + private OauthMemberClient getClient(String type) { + OauthServerType oauthServerType = OauthServerType.find(type); + return clients.get(oauthServerType); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/OauthInfoEntityConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/OauthInfoEntityConverter.java new file mode 100644 index 00000000..a3224b6d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/OauthInfoEntityConverter.java @@ -0,0 +1,24 @@ +package com.blackcompany.eeos.auth.application.domain.converter; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; +import com.blackcompany.eeos.common.support.converter.AbstractEntityConverter; +import org.springframework.stereotype.Component; + +@Component +public class OauthInfoEntityConverter + implements AbstractEntityConverter { + @Override + public OauthMemberModel from(final OauthInfoEntity oauthEntity) { + return OauthMemberModel.builder().oauthId(oauthEntity.getOauthId()).build(); + } + + @Override + public OauthInfoEntity toEntity(final OauthMemberModel oauthMemberModel) { + return OauthInfoEntity.builder().oauthId(oauthMemberModel.getOauthId()).build(); + } + + public OauthInfoEntity toEntity(final String oauthId, Long memberId) { + return OauthInfoEntity.builder().oauthId(oauthId).memberId(memberId).build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/TokenModelConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/TokenModelConverter.java new file mode 100644 index 00000000..ce24add7 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/converter/TokenModelConverter.java @@ -0,0 +1,19 @@ +package com.blackcompany.eeos.auth.application.domain.converter; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenModelConverter { + public TokenModel from( + String accessToken, Long accessExpiredTime, String refreshToken, Long refreshExpiredTime) { + return TokenModel.builder() + .accessToken(accessToken) + .accessExpiredTime(accessExpiredTime) + .refreshToken(refreshToken) + .refreshExpiredTime(refreshExpiredTime) + .build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenProvider.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenProvider.java new file mode 100644 index 00000000..524bb89e --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenProvider.java @@ -0,0 +1,54 @@ +package com.blackcompany.eeos.auth.application.domain.token; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import javax.crypto.SecretKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class TokenProvider { + + private static final String MEMBER_ID_CLAIM_KEY = "memberId"; + private final SecretKey secretKey; + private final long accessValidTime; + private final long refreshValidTime; + + public TokenProvider( + @Value("${security.jwt.token.secretKey}") String secretKey, + @Value("${security.jwt.token.access.validTime}") long accessValidTime, + @Value("${security.jwt.token.refresh.validTime}") long refreshValidTime) { + this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.accessValidTime = accessValidTime; + this.refreshValidTime = refreshValidTime; + } + + public String createAccessToken(final Long memberId) { + final Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .claim(MEMBER_ID_CLAIM_KEY, memberId) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + accessValidTime)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(final Long memberId) { + final Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .claim(MEMBER_ID_CLAIM_KEY, memberId) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + refreshValidTime)) + .signWith(secretKey) + .compact(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenResolver.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenResolver.java new file mode 100644 index 00000000..6d06fd04 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenResolver.java @@ -0,0 +1,47 @@ +package com.blackcompany.eeos.auth.application.domain.token; + +import com.blackcompany.eeos.auth.application.exception.TokenExpiredException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Objects; +import javax.crypto.SecretKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class TokenResolver { + + private static final String USER_ID_CLAIM_KEY = "memberId"; + private final SecretKey secretKey; + + public TokenResolver(@Value("${security.jwt.token.secretKey}") String accessSecretKey) { + this.secretKey = Keys.hmacShaKeyFor(accessSecretKey.getBytes(StandardCharsets.UTF_8)); + } + + private Claims getClaims(final String token) { + try { + return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody(); + } catch (ExpiredJwtException e) { + throw new TokenExpiredException(); + } + } + + public Long getExpiredDate(final String token) { + Objects.requireNonNull(token); + Date expiration = getClaims(token).getExpiration(); + + return expiration.getTime(); + } + + public Long getUserInfo(final String token) { + Objects.requireNonNull(token); + + return Long.valueOf(String.valueOf(getClaims(token).get(USER_ID_CLAIM_KEY))); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenValidator.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenValidator.java new file mode 100644 index 00000000..350b2c4a --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/domain/token/TokenValidator.java @@ -0,0 +1,22 @@ +package com.blackcompany.eeos.auth.application.domain.token; + +import com.blackcompany.eeos.auth.application.exception.TokenExpiredException; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenValidator { + private final TokenResolver tokenResolver; + + public void valid(String token) { + Long expired = tokenResolver.getExpiredDate(token); + Date expiredDate = new Date(expired); + + Date now = new Date(); + if (expiredDate.before(now)) { + throw new TokenExpiredException(); + } + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/converter/TokenResponseConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/converter/TokenResponseConverter.java new file mode 100644 index 00000000..252b9848 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/converter/TokenResponseConverter.java @@ -0,0 +1,14 @@ +package com.blackcompany.eeos.auth.application.dto.converter; + +import com.blackcompany.eeos.auth.application.dto.response.TokenResponse; +import org.springframework.stereotype.Component; + +@Component +public class TokenResponseConverter { + public TokenResponse from(String accessToken, Long accessExpiredTime) { + return TokenResponse.builder() + .accessToken(accessToken) + .accessExpiredTime(accessExpiredTime) + .build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/response/TokenResponse.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/response/TokenResponse.java new file mode 100644 index 00000000..9c25e95a --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/dto/response/TokenResponse.java @@ -0,0 +1,16 @@ +package com.blackcompany.eeos.auth.application.dto.response; + +import com.blackcompany.eeos.common.support.dto.AbstractResponseDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class TokenResponse implements AbstractResponseDto { + private String accessToken; + private Long accessExpiredTime; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/InvalidTokenException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/InvalidTokenException.java new file mode 100644 index 00000000..f87efbca --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/InvalidTokenException.java @@ -0,0 +1,18 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 토큰이 유효하지 않을 때 발생하는 예외 */ +public class InvalidTokenException extends BusinessException { + private static final String FAIL_CODE = "4003"; + + public InvalidTokenException() { + super(FAIL_CODE, HttpStatus.UNAUTHORIZED); + } + + @Override + public String getMessage() { + return "유효하지 않은 토큰입니다."; + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundCookieException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundCookieException.java new file mode 100644 index 00000000..7ba0db6c --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundCookieException.java @@ -0,0 +1,18 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 토큰이 쿠키에 존재하지 않을 때 발생하는 예외 */ +public class NotFoundCookieException extends BusinessException { + private static final String FAIL_CODE = "4004"; + + public NotFoundCookieException() { + super(FAIL_CODE, HttpStatus.UNAUTHORIZED); + } + + @Override + public String getMessage() { + return "리프레시 토큰이 존재하지 않습니다."; + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundHeaderTokenException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundHeaderTokenException.java new file mode 100644 index 00000000..c52a974d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundHeaderTokenException.java @@ -0,0 +1,18 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 토큰이 헤더에 존재하지 않을 때 발생하는 예외 */ +public class NotFoundHeaderTokenException extends BusinessException { + private static final String FAIL_CODE = "4000"; + + public NotFoundHeaderTokenException() { + super(FAIL_CODE, HttpStatus.UNAUTHORIZED); + } + + @Override + public String getMessage() { + return "엑세스 토큰이 존재하지 않습니다."; + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundOauthServerException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundOauthServerException.java new file mode 100644 index 00000000..62b51a36 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundOauthServerException.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** 존재하지 않는 oauth 서버일 때 발생하는 예외 */ +public class NotFoundOauthServerException extends BusinessException { + private static final String FAIL_CODE = "5000"; + private final String oauthServer; + + public NotFoundOauthServerException(String oauthServer) { + super(FAIL_CODE, HttpStatus.NOT_FOUND); + this.oauthServer = oauthServer; + } + + @Override + public String getMessage() { + return String.format("%s 는 존재하지 oauth 서버입니다.", oauthServer); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundUserException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundUserException.java new file mode 100644 index 00000000..b0f9a718 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/NotFoundUserException.java @@ -0,0 +1,12 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NotFoundUserException extends BusinessException { + private static final String MESSAGE = "존재하지 않은 유저입니다."; + + public NotFoundUserException() { + super(MESSAGE, HttpStatus.NOT_FOUND); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/TokenExpiredException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/TokenExpiredException.java new file mode 100644 index 00000000..e593afe5 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/exception/TokenExpiredException.java @@ -0,0 +1,12 @@ +package com.blackcompany.eeos.auth.application.exception; + +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class TokenExpiredException extends BusinessException { + private static final String MESSAGE = "토큰이 만료되었습니다."; + + public TokenExpiredException() { + super(MESSAGE, HttpStatus.FORBIDDEN); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthFacadeService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthFacadeService.java new file mode 100644 index 00000000..3f333cb2 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthFacadeService.java @@ -0,0 +1,24 @@ +package com.blackcompany.eeos.auth.application.service; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.TokenModel; +import com.blackcompany.eeos.auth.application.domain.client.OauthMemberClientComposite; +import com.blackcompany.eeos.auth.application.usecase.LoginUsecase; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthFacadeService implements LoginUsecase { + private final OauthMemberClientComposite oauthMemberClientComposite; + private final AuthService authService; + private final CreateTokenService createTokenService; + + @Override + public TokenModel login(String oauthServerType, String authCode) { + OauthMemberModel model = oauthMemberClientComposite.fetch(oauthServerType, authCode); + OauthInfoEntity entity = authService.login(model); + return createTokenService.execute(entity.getMemberId()); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthService.java new file mode 100644 index 00000000..343822f7 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/AuthService.java @@ -0,0 +1,43 @@ +package com.blackcompany.eeos.auth.application.service; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.converter.OauthInfoEntityConverter; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; +import com.blackcompany.eeos.auth.persistence.OauthInfoRepository; +import com.blackcompany.eeos.member.application.model.converter.MemberEntityConverter; +import com.blackcompany.eeos.member.persistence.MemberEntity; +import com.blackcompany.eeos.member.persistence.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AuthService { + + private final OauthInfoRepository oauthInfoRepository; + private final MemberRepository memberRepository; + private final MemberEntityConverter memberEntityConverter; + private final OauthInfoEntityConverter oauthInfoEntityConverter; + + @Transactional + public OauthInfoEntity login(final OauthMemberModel model) { + return oauthInfoRepository + .findByOauthId(model.getOauthId()) + .orElseGet(() -> signUpMember(model)); + } + + private OauthInfoEntity signUpMember(final OauthMemberModel model) { + MemberEntity entity = + memberEntityConverter.toEntity(model.getName(), model.getOauthServerType()); + + MemberEntity savedMember = memberRepository.save(entity); + return createOauthInfoEntity(model.getOauthId(), savedMember.getId()); + } + + private OauthInfoEntity createOauthInfoEntity(final String oauthId, final Long memberId) { + OauthInfoEntity entity = oauthInfoEntityConverter.toEntity(oauthId, memberId); + return oauthInfoRepository.save(entity); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/CreateTokenService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/CreateTokenService.java new file mode 100644 index 00000000..6243a28d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/CreateTokenService.java @@ -0,0 +1,42 @@ +package com.blackcompany.eeos.auth.application.service; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; +import com.blackcompany.eeos.auth.application.domain.converter.TokenModelConverter; +import com.blackcompany.eeos.auth.application.domain.token.TokenProvider; +import com.blackcompany.eeos.auth.application.domain.token.TokenResolver; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntity; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntityConverter; +import com.blackcompany.eeos.auth.persistence.AuthInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CreateTokenService { + private final TokenProvider tokenProvider; + private final TokenModelConverter tokenModelConverter; + private final TokenResolver tokenResolver; + private final AuthInfoRepository authInfoRepository; + private final AuthInfoEntityConverter authInfoEntityConverter; + + @Transactional + public TokenModel execute(final Long memberId) { + String accessToken = tokenProvider.createAccessToken(memberId); + String refreshToken = tokenProvider.createRefreshToken(memberId); + + saveToken(memberId, refreshToken); + + return tokenModelConverter.from( + accessToken, + tokenResolver.getExpiredDate(accessToken), + refreshToken, + tokenResolver.getExpiredDate(refreshToken)); + } + + private void saveToken(final Long memberId, final String token) { + AuthInfoEntity authInfoEntity = authInfoEntityConverter.from(memberId, token); + authInfoRepository.save(authInfoEntity); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/ReissueService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/ReissueService.java new file mode 100644 index 00000000..d7fe090d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/service/ReissueService.java @@ -0,0 +1,47 @@ +package com.blackcompany.eeos.auth.application.service; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; +import com.blackcompany.eeos.auth.application.domain.token.TokenResolver; +import com.blackcompany.eeos.auth.application.exception.InvalidTokenException; +import com.blackcompany.eeos.auth.application.usecase.ReissueUsecase; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntity; +import com.blackcompany.eeos.auth.persistence.AuthInfoRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class ReissueService implements ReissueUsecase { + private final CreateTokenService createTokenService; + private final AuthInfoRepository authInfoRepository; + private final TokenResolver tokenResolver; + + @Transactional + @Override + public TokenModel execute(final String token) { + Long memberId = tokenResolver.getUserInfo(token); + validateToken(memberId, token); + + return createTokenService.execute(memberId); + } + + private void validateToken(final Long memberId, final String token) { + Optional validToken = + authInfoRepository.findByMemberIdAndToken(memberId, token); + if (validToken.isPresent()) { + validToken.ifPresent(authInfoRepository::delete); + return; + } + deleteInvalidToken(token); + } + + private void deleteInvalidToken(final String token) { + authInfoRepository.findByToken(token).ifPresent(authInfoRepository::delete); + throw new InvalidTokenException(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/LoginUsecase.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/LoginUsecase.java new file mode 100644 index 00000000..c3e74529 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/LoginUsecase.java @@ -0,0 +1,7 @@ +package com.blackcompany.eeos.auth.application.usecase; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; + +public interface LoginUsecase { + TokenModel login(String oauthServerType, String authCode); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/ReissueUsecase.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/ReissueUsecase.java new file mode 100644 index 00000000..113d4c8d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/application/usecase/ReissueUsecase.java @@ -0,0 +1,7 @@ +package com.blackcompany.eeos.auth.application.usecase; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; + +public interface ReissueUsecase { + TokenModel execute(final String token); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClient.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClient.java new file mode 100644 index 00000000..73d5b58b --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClient.java @@ -0,0 +1,10 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.client; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken; + +public interface SlackApiClient { + SlackToken fetchToken(String client, String code, String clientSecret); + + SlackMember fetchMember(String token); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClientImpl.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClientImpl.java new file mode 100644 index 00000000..fa7d59e7 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackApiClientImpl.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.client; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "SlackOpenFeign", url = "https://slack.com/api") +public interface SlackApiClientImpl extends SlackApiClient { + @GetMapping(path = "/oauth.v2.access") + SlackToken fetchToken( + @RequestParam("client_id") String clientId, + @RequestParam("code") String code, + @RequestParam("client_secret") String clientSecret); + + @GetMapping(path = "/users.profile.get") + SlackMember fetchMember(@RequestHeader("Authorization") String token); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClient.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClient.java new file mode 100644 index 00000000..514a91c6 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClient.java @@ -0,0 +1,80 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.client; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import com.blackcompany.eeos.auth.application.domain.client.OauthMemberClient; +import com.blackcompany.eeos.auth.infra.oauth.slack.config.SlackOauthConfig; +import com.blackcompany.eeos.auth.infra.oauth.slack.converter.OauthModelConverter; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackApiResponse; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken; +import com.blackcompany.eeos.auth.infra.oauth.slack.exception.SlackApiException; +import com.blackcompany.eeos.auth.infra.oauth.slack.support.SlackQuery; +import com.blackcompany.eeos.auth.infra.oauth.slack.support.SlackTriQuery; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SlackOauthMemberClient implements OauthMemberClient { + private static final String TOKEN_METHOD_NAME = "getToken"; + private static final String MEMBER_INFO_METHOD_NAME = "getMemberInfo"; + private static final String BEARER = "Bearer"; + + private final SlackOauthConfig oauthConfig; + private final SlackApiClient slackApiClient; + private final OauthModelConverter oauthModelConverter; + + @Override + public OauthServerType support() { + return OauthServerType.SLACK; + } + + @Override + public OauthMemberModel fetch(String code) { + SlackToken slackToken = + execute( + slackApiClient::fetchToken, + TOKEN_METHOD_NAME, + oauthConfig.getClientId(), + code, + oauthConfig.getClientSecret()); + + SlackMember slackMember = + execute( + slackApiClient::fetchMember, + MEMBER_INFO_METHOD_NAME, + requestToken(slackToken.getToken())); + + return oauthModelConverter.from( + slackToken.getUserId(), slackMember.getName(), OauthServerType.SLACK); + } + + private T execute( + final SlackTriQuery slackFunction, + final String methodName, + final K param1, + final U param2, + final R param3) { + T result = slackFunction.execute(param1, param2, param3); + validateResponse(methodName, result); + return result; + } + + private T execute( + final SlackQuery slackFunction, final String methodName, final K request) { + T result = slackFunction.execute(request); + validateResponse(methodName, result); + return result; + } + + private String requestToken(String token) { + return String.format("%s %s", BEARER, token); + } + + private void validateResponse(String methodName, T response) { + if (!response.isOk()) { + throw new SlackApiException(methodName, response); + } + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/config/SlackOauthConfig.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/config/SlackOauthConfig.java new file mode 100644 index 00000000..5bca85a8 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/config/SlackOauthConfig.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@PropertySource("classpath:/env.properties") +@Configuration +@Setter +@Getter +public class SlackOauthConfig { + + @Value("${oauth.provider.slack.clientId}") + private String clientId; + + @Value("${oauth.provider.slack.clientSecret}") + private String clientSecret; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/converter/OauthModelConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/converter/OauthModelConverter.java new file mode 100644 index 00000000..cad97a96 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/converter/OauthModelConverter.java @@ -0,0 +1,17 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.converter; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import org.springframework.stereotype.Component; + +@Component +public class OauthModelConverter { + public OauthMemberModel from( + final String oauthId, final String name, final OauthServerType type) { + return OauthMemberModel.builder() + .oauthId(oauthId) + .name(name) + .oauthServerType(OauthServerType.find(type)) + .build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackApiResponse.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackApiResponse.java new file mode 100644 index 00000000..8e0b7868 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackApiResponse.java @@ -0,0 +1,7 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.dto; + +public interface SlackApiResponse { + boolean isOk(); + + String getError(); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackErrorApiResponse.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackErrorApiResponse.java new file mode 100644 index 00000000..ea7f2b8d --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackErrorApiResponse.java @@ -0,0 +1,15 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class SlackErrorApiResponse implements SlackApiResponse { + private boolean ok; + private String error; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackMember.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackMember.java new file mode 100644 index 00000000..7ae61f68 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackMember.java @@ -0,0 +1,38 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@Builder +@NoArgsConstructor +public class SlackMember implements SlackApiResponse { + private boolean ok; + + @JsonProperty("profile") + private UserProfile profile; + + private String error; + + @Override + public String getError() { + return error; + } + + @Getter + @AllArgsConstructor + @Builder + @NoArgsConstructor + public static class UserProfile { + @JsonProperty("display_name") + private String displayName; + } + + public String getName() { + return getProfile().getDisplayName(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackToken.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackToken.java new file mode 100644 index 00000000..895e54d6 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/dto/SlackToken.java @@ -0,0 +1,46 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SlackToken implements SlackApiResponse { + @JsonProperty("ok") + private boolean ok; + + @JsonProperty("authed_user") + private AuthedUser authedUser; + + private String error; + + @Override + public String getError() { + return error; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AuthedUser { + @JsonProperty("id") + private String userId; + + @JsonProperty("access_token") + private String accessToken; + } + + public String getUserId() { + return getAuthedUser().getUserId(); + } + + public String getToken() { + return getAuthedUser().getAccessToken(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/exception/SlackApiException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/exception/SlackApiException.java new file mode 100644 index 00000000..1c320ba3 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/exception/SlackApiException.java @@ -0,0 +1,25 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.exception; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackApiResponse; +import com.blackcompany.eeos.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +/** slack API에서 실패 응답을 받을 시 발생하는 예외 */ +public class SlackApiException extends BusinessException { + + private static final String FAIL_CODE = "5000"; + private final String apiMethod; + private final SlackApiResponse slackApiResponse; + + public SlackApiException(final String apiMethod, final SlackApiResponse slackApiResponse) { + super(FAIL_CODE, HttpStatus.BAD_REQUEST); + this.apiMethod = apiMethod; + this.slackApiResponse = slackApiResponse; + } + + @Override + public String getMessage() { + return String.format( + "슬랙 API 호출 실패 : %s api 호출 , %s로 응답", apiMethod, slackApiResponse.getError()); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackQuery.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackQuery.java new file mode 100644 index 00000000..5b76a269 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackQuery.java @@ -0,0 +1,8 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.support; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackApiResponse; + +@FunctionalInterface +public interface SlackQuery { + T execute(K request); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackTriQuery.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackTriQuery.java new file mode 100644 index 00000000..a6122790 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/infra/oauth/slack/support/SlackTriQuery.java @@ -0,0 +1,8 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.support; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackApiResponse; + +@FunctionalInterface +public interface SlackTriQuery { + T execute(K k, U u, V v); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntity.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntity.java new file mode 100644 index 00000000..030e5546 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntity.java @@ -0,0 +1,38 @@ +package com.blackcompany.eeos.auth.persistence; + +import com.blackcompany.eeos.common.persistence.BaseEntity; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@SuperBuilder(toBuilder = true) +@Entity +@Table(name = AuthInfoEntity.ENTITY_PREFIX) +public class AuthInfoEntity extends BaseEntity { + + public static final String ENTITY_PREFIX = "auth_info"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", nullable = false) + private Long id; + + @Column(name = ENTITY_PREFIX + "_token", nullable = false) + private String token; + + @Column(name = ENTITY_PREFIX + "_member_id", nullable = false) + private Long memberId; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntityConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntityConverter.java new file mode 100644 index 00000000..77132522 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoEntityConverter.java @@ -0,0 +1,11 @@ +package com.blackcompany.eeos.auth.persistence; + +import org.springframework.stereotype.Component; + +@Component +public class AuthInfoEntityConverter { + + public AuthInfoEntity from(Long memberId, String refreshToken) { + return AuthInfoEntity.builder().memberId(memberId).token(refreshToken).build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoRepository.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoRepository.java new file mode 100644 index 00000000..bbea04c6 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/AuthInfoRepository.java @@ -0,0 +1,10 @@ +package com.blackcompany.eeos.auth.persistence; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AuthInfoRepository extends JpaRepository { + Optional findByMemberIdAndToken(Long memberId, String token); + + Optional findByToken(String token); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoEntity.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoEntity.java new file mode 100644 index 00000000..0cc4ea38 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoEntity.java @@ -0,0 +1,37 @@ +package com.blackcompany.eeos.auth.persistence; + +import com.blackcompany.eeos.common.persistence.BaseEntity; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@SuperBuilder(toBuilder = true) +@Entity +@Table(name = OauthInfoEntity.ENTITY_PREFIX) +public class OauthInfoEntity extends BaseEntity { + public static final String ENTITY_PREFIX = "oauth_info"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", nullable = false) + private Long id; + + @Column(name = ENTITY_PREFIX + "_oauth_id", nullable = false) + private String oauthId; + + @Column(name = ENTITY_PREFIX + "_member_id", nullable = false) + private Long memberId; +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoRepository.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoRepository.java new file mode 100644 index 00000000..be2fb633 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/persistence/OauthInfoRepository.java @@ -0,0 +1,8 @@ +package com.blackcompany.eeos.auth.persistence; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OauthInfoRepository extends JpaRepository { + Optional findByOauthId(String oauthId); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/LoginConfig.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/LoginConfig.java new file mode 100644 index 00000000..41d98556 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/LoginConfig.java @@ -0,0 +1,42 @@ +package com.blackcompany.eeos.auth.presentation; + +import com.blackcompany.eeos.auth.application.domain.token.TokenValidator; +import com.blackcompany.eeos.auth.presentation.interceptor.AuthInterceptor; +import com.blackcompany.eeos.auth.presentation.support.CookieTokenExtractor; +import com.blackcompany.eeos.auth.presentation.support.HeaderTokenExtractor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class LoginConfig implements WebMvcConfigurer { + private final TokenValidator tokenValidator; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry + .addInterceptor(memberAuthInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns("/api/auth/**"); + registry.addInterceptor(reissueAuthInterceptor()).addPathPatterns("/auth/reissue"); + } + + @Bean + public AuthInterceptor memberAuthInterceptor() { + return AuthInterceptor.builder() + .tokenExtractor(new HeaderTokenExtractor()) + .tokenValidator(tokenValidator) + .build(); + } + + @Bean + public AuthInterceptor reissueAuthInterceptor() { + return AuthInterceptor.builder() + .tokenExtractor(new CookieTokenExtractor()) + .tokenValidator(tokenValidator) + .build(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java new file mode 100644 index 00000000..a6b80021 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/interceptor/AuthInterceptor.java @@ -0,0 +1,56 @@ +package com.blackcompany.eeos.auth.presentation.interceptor; + +import com.blackcompany.eeos.auth.application.domain.token.TokenValidator; +import com.blackcompany.eeos.auth.presentation.support.TokenExtractor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class AuthInterceptor implements HandlerInterceptor { + private final TokenExtractor tokenExtractor; + private final TokenValidator tokenValidator; + + public static AuthInterceptorBuilder builder() { + return new AuthInterceptorBuilder(); + } + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + + canExtractToken(request); + return true; + } + + private void canExtractToken(HttpServletRequest request) { + String token = tokenExtractor.extract(request); + tokenValidator.valid(token); + } + + public static class AuthInterceptorBuilder { + + private TokenExtractor tokenExtractor; + private TokenValidator tokenValidator; + + public AuthInterceptorBuilder tokenExtractor(TokenExtractor tokenExtractor) { + this.tokenExtractor = tokenExtractor; + return this; + } + + public AuthInterceptorBuilder tokenValidator(TokenValidator tokenValidator) { + this.tokenValidator = tokenValidator; + return this; + } + + public AuthInterceptor build() { + return new AuthInterceptor(tokenExtractor, tokenValidator); + } + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractor.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractor.java new file mode 100644 index 00000000..c887e94b --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractor.java @@ -0,0 +1,42 @@ +package com.blackcompany.eeos.auth.presentation.support; + +import com.blackcompany.eeos.auth.application.exception.NotFoundCookieException; +import java.util.Objects; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +@Component("cookie") +public class CookieTokenExtractor implements TokenExtractor { + private static final String COOKIE_KEY = "token"; + + @Override + public String extract(HttpServletRequest request) { + Cookie[] cookies = getCookies(request); + + for (Cookie cookie : cookies) { + if (Objects.equals(COOKIE_KEY, cookie.getName())) { + return getValue(cookie.getValue()); + } + } + + throw new NotFoundCookieException(); + } + + private Cookie[] getCookies(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + + if (cookies == null) { + throw new NotFoundCookieException(); + } + + return cookies; + } + + private String getValue(String value) { + if (value != null) { + return value; + } + throw new NotFoundCookieException(); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractor.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractor.java new file mode 100644 index 00000000..95bf485e --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractor.java @@ -0,0 +1,31 @@ +package com.blackcompany.eeos.auth.presentation.support; + +import com.blackcompany.eeos.auth.application.exception.NotFoundHeaderTokenException; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; + +@Component +public class HeaderTokenExtractor implements TokenExtractor { + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + + @Override + public String extract(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null) { + throw new NotFoundHeaderTokenException(); + } + return extractToken(header); + } + + private String extractToken(String header) { + validateHeader(header); + return header.substring(BEARER_TOKEN_PREFIX.length()).trim(); + } + + private void validateHeader(String header) { + if (!header.toLowerCase().startsWith(BEARER_TOKEN_PREFIX.toLowerCase())) { + throw new NotFoundHeaderTokenException(); + } + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/TokenExtractor.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/TokenExtractor.java new file mode 100644 index 00000000..1f83e496 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/support/TokenExtractor.java @@ -0,0 +1,8 @@ +package com.blackcompany.eeos.auth.presentation.support; + +import javax.servlet.http.HttpServletRequest; + +public interface TokenExtractor { + + String extract(HttpServletRequest request); +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/web/controller/AuthController.java b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/web/controller/AuthController.java new file mode 100644 index 00000000..0d1654b6 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/auth/presentation/web/controller/AuthController.java @@ -0,0 +1,89 @@ +package com.blackcompany.eeos.auth.presentation.web.controller; + +import com.blackcompany.eeos.auth.application.domain.TokenModel; +import com.blackcompany.eeos.auth.application.dto.converter.TokenResponseConverter; +import com.blackcompany.eeos.auth.application.dto.response.TokenResponse; +import com.blackcompany.eeos.auth.application.usecase.LoginUsecase; +import com.blackcompany.eeos.auth.application.usecase.ReissueUsecase; +import com.blackcompany.eeos.auth.presentation.support.TokenExtractor; +import com.blackcompany.eeos.common.presentation.respnose.ApiResponse; +import com.blackcompany.eeos.common.presentation.respnose.ApiResponseBody.SuccessBody; +import com.blackcompany.eeos.common.presentation.respnose.ApiResponseGenerator; +import com.blackcompany.eeos.common.presentation.respnose.MessageCode; +import com.blackcompany.eeos.common.utils.TimeUtil; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + private static final String COOKIE_KEY = "token"; + + private final LoginUsecase loginUsecase; + private final ReissueUsecase reissueUsecase; + private final TokenExtractor tokenExtractor; + private final TokenResponseConverter tokenResponseConverter; + + private final String domain; + + public AuthController( + LoginUsecase loginUsecase, + ReissueUsecase reissueUsecase, + @Qualifier("cookie") TokenExtractor tokenExtractor, + TokenResponseConverter tokenResponseConverter, + @Value("${api.domain}") String domain) { + this.loginUsecase = loginUsecase; + this.reissueUsecase = reissueUsecase; + this.tokenExtractor = tokenExtractor; + this.tokenResponseConverter = tokenResponseConverter; + this.domain = domain; + } + + @PostMapping("/login/{oauthServerType}") + ApiResponse> login( + @PathVariable String oauthServerType, + @RequestParam("code") String code, + HttpServletResponse httpResponse) { + TokenModel tokenModel = loginUsecase.login(oauthServerType, code); + TokenResponse response = toResponse(tokenModel, httpResponse); + + return ApiResponseGenerator.success(response, HttpStatus.CREATED, MessageCode.CREATE); + } + + @PostMapping("/reissue") + ApiResponse> reissue( + HttpServletRequest request, HttpServletResponse httpResponse) { + String token = tokenExtractor.extract(request); + TokenModel tokenModel = reissueUsecase.execute(token); + TokenResponse response = toResponse(tokenModel, httpResponse); + + return ApiResponseGenerator.success(response, HttpStatus.CREATED, MessageCode.CREATE); + } + + private TokenResponse toResponse(TokenModel tokenModel, HttpServletResponse httpResponse) { + TokenResponse response = + tokenResponseConverter.from(tokenModel.getAccessToken(), tokenModel.getAccessExpiredTime()); + setCookie(httpResponse, tokenModel); + + return response; + } + + private void setCookie(HttpServletResponse response, TokenModel tokenModel) { + Cookie cookie = new Cookie(COOKIE_KEY, tokenModel.getRefreshToken()); + cookie.setDomain(domain); + cookie.setPath("/"); + cookie.setMaxAge(TimeUtil.convertSecondsFromMillis(tokenModel.getRefreshExpiredTime())); + cookie.setHttpOnly(false); + cookie.setSecure(false); + response.addCookie(cookie); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/common/exception/BusinessException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/common/exception/BusinessException.java index edad0f02..55643f10 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/common/exception/BusinessException.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/common/exception/BusinessException.java @@ -5,10 +5,11 @@ @Getter public class BusinessException extends RuntimeException { + private final String code; private final HttpStatus httpStatus; - public BusinessException(String message, HttpStatus httpStatus) { - super(message); + public BusinessException(String code, HttpStatus httpStatus) { + this.code = code; this.httpStatus = httpStatus; } } diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/common/presentation/respnose/ApiResponseGenerator.java b/BE/eeos/src/main/java/com/blackcompany/eeos/common/presentation/respnose/ApiResponseGenerator.java index 29bb9da2..fdebe49d 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/common/presentation/respnose/ApiResponseGenerator.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/common/presentation/respnose/ApiResponseGenerator.java @@ -25,14 +25,6 @@ public static ApiResponse> success( new ApiResponseBody.SuccessBody<>(data, code.getMessage(), code.getCode()), status); } - public static ApiResponse> success( - final D data, final HttpStatus status, MessageCode code, String cookieValue) { - return new ApiResponse<>( - new ApiResponseBody.SuccessBody<>(data, code.getMessage(), code.getCode()), - setCookie(cookieValue), - status); - } - public static ApiResponse>> success( final Page data, final HttpStatus status, final MessageCode code) { return new ApiResponse<>( diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/common/utils/TimeUtil.java b/BE/eeos/src/main/java/com/blackcompany/eeos/common/utils/TimeUtil.java new file mode 100644 index 00000000..49fea780 --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/common/utils/TimeUtil.java @@ -0,0 +1,9 @@ +package com.blackcompany.eeos.common.utils; + +public class TimeUtil { + private TimeUtil() {} + + public static int convertSecondsFromMillis(long milliseconds) { + return (int) (milliseconds / 1000); + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/config/OpenFeignConfig.java b/BE/eeos/src/main/java/com/blackcompany/eeos/config/OpenFeignConfig.java new file mode 100644 index 00000000..f7e31caf --- /dev/null +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/config/OpenFeignConfig.java @@ -0,0 +1,15 @@ +package com.blackcompany.eeos.config; + +import feign.Logger; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients("com.blackcompany.eeos") +public class OpenFeignConfig { + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundMemberException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/exception/NotFoundMemberException.java similarity index 81% rename from BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundMemberException.java rename to BE/eeos/src/main/java/com/blackcompany/eeos/member/application/exception/NotFoundMemberException.java index 2cf927c4..ccf4f24f 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/attend/application/exception/NotFoundMemberException.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/exception/NotFoundMemberException.java @@ -1,4 +1,4 @@ -package com.blackcompany.eeos.attend.application.exception; +package com.blackcompany.eeos.member.application.exception; import com.blackcompany.eeos.common.exception.BusinessException; import org.springframework.http.HttpStatus; diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/model/converter/MemberEntityConverter.java b/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/model/converter/MemberEntityConverter.java index efdc6e97..a532ac3c 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/model/converter/MemberEntityConverter.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/member/application/model/converter/MemberEntityConverter.java @@ -1,5 +1,6 @@ package com.blackcompany.eeos.member.application.model.converter; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; import com.blackcompany.eeos.common.support.converter.AbstractEntityConverter; import com.blackcompany.eeos.member.application.model.MemberModel; import com.blackcompany.eeos.member.persistence.MemberEntity; @@ -10,19 +11,15 @@ public class MemberEntityConverter implements AbstractEntityConverter { @Query( - "SELECT m FROM MemberEntity m inner join AttendEntity a on m.id = a.memberId where a.programId = :programId ORDER BY m.generation, m.name") + "SELECT m FROM MemberEntity m inner join AttendEntity a on m.id = a.memberId where a.programId = :programId ORDER BY m.name") List findMembersByProgramId(@Param("programId") Long programId); } diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/exception/NotFoundProgramException.java b/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/exception/NotFoundProgramException.java index 63a66390..e2207f07 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/exception/NotFoundProgramException.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/exception/NotFoundProgramException.java @@ -3,8 +3,18 @@ import com.blackcompany.eeos.common.exception.BusinessException; import org.springframework.http.HttpStatus; +/** 존재하지 않는 프로그램일 때 발생하는 예외 */ public class NotFoundProgramException extends BusinessException { - public NotFoundProgramException() { - super("존재하지 않는 프로그램입니다.", HttpStatus.NOT_FOUND); + private static final String FAIL_CODE = "1000"; + private final Long programId; + + public NotFoundProgramException(Long programId) { + super(FAIL_CODE, HttpStatus.NOT_FOUND); + this.programId = programId; + } + + @Override + public String getMessage() { + return String.format("존재하지 않는 프로그램입니다. programId : %d", programId); } } diff --git a/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramValidService.java b/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramValidService.java index 872ad281..e03398e9 100644 --- a/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramValidService.java +++ b/BE/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramValidService.java @@ -13,7 +13,7 @@ public class ProgramValidService { public void validate(Long programId) { if (!programRepository.existsById(programId)) { - throw new NotFoundProgramException(); + throw new NotFoundProgramException(programId); } } } diff --git a/BE/eeos/src/main/resources/application-api.yml b/BE/eeos/src/main/resources/application-api.yml new file mode 100644 index 00000000..4560f7ab --- /dev/null +++ b/BE/eeos/src/main/resources/application-api.yml @@ -0,0 +1,11 @@ +spring: + config: + activate: + on-profile: api + +cors: + allow-origin: + urls: ${CORS_URL} + +api: + domain: ${API_DOMAIN} \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-dev-api.yml b/BE/eeos/src/main/resources/application-dev-api.yml deleted file mode 100644 index 5cc491cf..00000000 --- a/BE/eeos/src/main/resources/application-dev-api.yml +++ /dev/null @@ -1,3 +0,0 @@ -cors: - allow-origin: - urls: https://fe.dev.eeos.store/ \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-local-api.yml b/BE/eeos/src/main/resources/application-local-api.yml deleted file mode 100644 index e6e543f4..00000000 --- a/BE/eeos/src/main/resources/application-local-api.yml +++ /dev/null @@ -1,3 +0,0 @@ -cors: - allow-origin: - urls: https://localhost:3000/ \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-local-mysql.yml b/BE/eeos/src/main/resources/application-local-mysql.yml index 0adde975..0020b401 100644 --- a/BE/eeos/src/main/resources/application-local-mysql.yml +++ b/BE/eeos/src/main/resources/application-local-mysql.yml @@ -9,7 +9,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: validate + ddl-auto: create properties: hibernate: format_sql: true diff --git a/BE/eeos/src/main/resources/application-local-token.yml b/BE/eeos/src/main/resources/application-local-token.yml new file mode 100644 index 00000000..37220479 --- /dev/null +++ b/BE/eeos/src/main/resources/application-local-token.yml @@ -0,0 +1,13 @@ +spring: + config: + activate: + on-profile: local-token + +security: + jwt: + token: + secretKey: asccesssecretkeyoverflowsecrekey + access: + validTime: 1800000 + refresh: + validTime: 3600000 \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-dev-mysql.yml b/BE/eeos/src/main/resources/application-mysql.yml similarity index 92% rename from BE/eeos/src/main/resources/application-dev-mysql.yml rename to BE/eeos/src/main/resources/application-mysql.yml index c5770573..2b858e18 100644 --- a/BE/eeos/src/main/resources/application-dev-mysql.yml +++ b/BE/eeos/src/main/resources/application-mysql.yml @@ -1,7 +1,7 @@ spring: config: activate: - on-profile: dev-mysql + on-profile: mysql datasource: url: ${DATASOURCE_URL} username: ${DATASOURCE_USERNAME} diff --git a/BE/eeos/src/main/resources/application-oauth.yml b/BE/eeos/src/main/resources/application-oauth.yml new file mode 100644 index 00000000..d7dafd83 --- /dev/null +++ b/BE/eeos/src/main/resources/application-oauth.yml @@ -0,0 +1,9 @@ +spring: + config: + activate: + on-profile: oauth +oauth: + provider: + slack: + clientId: ${SLACK_CLIENT_ID} + clientSecret: ${SLACK_CLIENT_SECRET} \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-prod-api.yml b/BE/eeos/src/main/resources/application-prod-api.yml deleted file mode 100644 index f31f245b..00000000 --- a/BE/eeos/src/main/resources/application-prod-api.yml +++ /dev/null @@ -1,3 +0,0 @@ -cors: - allow-origin: - urls: https://econo.eeos.store/ \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-prod-mysql.yml b/BE/eeos/src/main/resources/application-prod-mysql.yml deleted file mode 100644 index c1dc6f71..00000000 --- a/BE/eeos/src/main/resources/application-prod-mysql.yml +++ /dev/null @@ -1,19 +0,0 @@ -spring: - config: - activate: - on-profile: prod-mysql - datasource: - url: ${DATASOURCE_URL} - username: ${DATASOURCE_USERNAME} - password: ${DATASOURCE_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - jpa: - hibernate: - ddl-auto: validate - properties: - hibernate: - format_sql: true - -logging: - level: - sql: debug \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application-token.yml b/BE/eeos/src/main/resources/application-token.yml new file mode 100644 index 00000000..c40dabd0 --- /dev/null +++ b/BE/eeos/src/main/resources/application-token.yml @@ -0,0 +1,13 @@ +spring: + config: + activate: + on-profile: token + +security: + jwt: + token: + secretKey: ${TOKEN_SECRET_KEY} + access: + validTime: ${ACCESS_VALID_TIME} + refresh: + validTime: ${REFRESH_VALID_TIME} \ No newline at end of file diff --git a/BE/eeos/src/main/resources/application.yml b/BE/eeos/src/main/resources/application.yml index b3045fd2..133e09bf 100644 --- a/BE/eeos/src/main/resources/application.yml +++ b/BE/eeos/src/main/resources/application.yml @@ -3,11 +3,17 @@ spring: group: local: - local-mysql - - local-api + - api + - oauth + - local-token dev: - - dev-mysql - - dev-api + - mysql + - api + - oauth + - token prod: - - prod-mysql - - prod-api + - mysql + - api + - oauth + - token active: local \ No newline at end of file diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientCompositeTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientCompositeTest.java new file mode 100644 index 00000000..d8f5e450 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/domain/client/OauthMemberClientCompositeTest.java @@ -0,0 +1,35 @@ +package com.blackcompany.eeos.auth.application.domain.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.fixture.FakeOauthMemberClient; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class OauthMemberClientCompositeTest { + @Test + @DisplayName("전달받은 인증 서버에게 인증된 유저 정보를 요청하여 유저 정보를 받는다.") + void fetch() { + // given + String oauthServerType = "fake"; + String authCode = "code"; + Set set = new HashSet<>(); + FakeOauthMemberClient fakeOauthMemberClient = new FakeOauthMemberClient(); + set.add(fakeOauthMemberClient); + + OauthMemberClientComposite oauthMemberClientComposite = new OauthMemberClientComposite(set); + + // when + OauthMemberModel oauthMemberModel = oauthMemberClientComposite.fetch(oauthServerType, authCode); + + // then + assertEquals(oauthMemberModel.getOauthId(), fakeOauthMemberClient.fetch(authCode).getOauthId()); + assertEquals(oauthMemberModel.getName(), fakeOauthMemberClient.fetch(authCode).getName()); + assertEquals( + oauthMemberModel.getOauthServerType(), + fakeOauthMemberClient.fetch(authCode).getOauthServerType()); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthFacadeServiceTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthFacadeServiceTest.java new file mode 100644 index 00000000..dc13e491 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthFacadeServiceTest.java @@ -0,0 +1,48 @@ +package com.blackcompany.eeos.auth.application.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.client.OauthMemberClientComposite; +import com.blackcompany.eeos.auth.fixture.FakeOauthMember; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AuthFacadeServiceTest { + @Mock OauthMemberClientComposite oauthMemberClientComposite; + + @Mock CreateTokenService createTokenService; + + @Mock AuthService authService; + + @InjectMocks AuthFacadeService authFacadeService; + + @Test + @DisplayName("로그인 요청이 들어오면 토큰을 반환한다.") + void response_token() { + // given + String type = "type"; + String authCode = "code"; + Long memberId = 1L; + + OauthMemberModel oauthMemberModel = FakeOauthMember.oauthMemberModel(); + OauthInfoEntity oauthInfoEntity = FakeOauthMember.oauthInfoEntity(); + + when(oauthMemberClientComposite.fetch(type, authCode)).thenReturn(oauthMemberModel); + when(authService.login(oauthMemberModel)).thenReturn(oauthInfoEntity); + + // when + authFacadeService.login(type, authCode); + + // then + Mockito.verify(createTokenService).execute(memberId); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthServiceTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthServiceTest.java new file mode 100644 index 00000000..70561e60 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/AuthServiceTest.java @@ -0,0 +1,64 @@ +package com.blackcompany.eeos.auth.application.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.domain.converter.OauthInfoEntityConverter; +import com.blackcompany.eeos.auth.fixture.FakeMember; +import com.blackcompany.eeos.auth.fixture.FakeOauthMember; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; +import com.blackcompany.eeos.auth.persistence.OauthInfoRepository; +import com.blackcompany.eeos.member.application.model.converter.MemberEntityConverter; +import com.blackcompany.eeos.member.persistence.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + @Mock OauthInfoRepository oauthInfoRepository; + @InjectMocks AuthService authService; + @Mock MemberRepository memberRepository; + @Spy OauthInfoEntityConverter oauthInfoEntityConverter; + + @Spy MemberEntityConverter memberEntityConverter; + + @Test + @DisplayName("새로운 회원인 경우 oauth에서 가져온 회원 정보를 저장한다.") + void login_existing_user() { + // given + when(oauthInfoRepository.findByOauthId(FakeOauthMember.oauthMemberModel().getOauthId())) + .thenReturn(Optional.ofNullable(null)); + when(memberRepository.save(Mockito.any())).thenReturn(FakeMember.memberEntity()); + + // when + authService.login(FakeOauthMember.oauthMemberModel()); + + // then + assertAll( + () -> verify(memberRepository).save(Mockito.any()), + () -> verify(oauthInfoRepository).save(Mockito.any())); + } + + @Test + @DisplayName("기존 회원인 경우 존재하던 oauth 정보를 가져온다.") + void login_new_user() { + // given + when(oauthInfoRepository.findByOauthId(FakeOauthMember.oauthMemberModel().getOauthId())) + .thenReturn(Optional.of(FakeOauthMember.oauthInfoEntity())); + + // when + OauthInfoEntity entity = authService.login(FakeOauthMember.oauthMemberModel()); + + // then + assertEquals(entity.getOauthId(), FakeOauthMember.oauthMemberModel().getOauthId()); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/CreateTokenServiceTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/CreateTokenServiceTest.java new file mode 100644 index 00000000..880deb38 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/CreateTokenServiceTest.java @@ -0,0 +1,51 @@ +package com.blackcompany.eeos.auth.application.service; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.domain.converter.TokenModelConverter; +import com.blackcompany.eeos.auth.application.domain.token.TokenProvider; +import com.blackcompany.eeos.auth.application.domain.token.TokenResolver; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntity; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntityConverter; +import com.blackcompany.eeos.auth.persistence.AuthInfoRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateTokenServiceTest { + @InjectMocks CreateTokenService createTokenService; + @Mock TokenResolver tokenResolver; + @Spy TokenModelConverter tokeModelConverter; + + @Mock TokenProvider tokenProvider; + @Mock AuthInfoRepository authInfoRepository; + @Mock AuthInfoEntityConverter authInfoEntityConverter; + + @Test + @DisplayName("토큰 생성 요청이 들어오면 토큰을 생성한 후 토큰을 저장한다.") + void save_token_when_request_create_token() { + // given + Long userId = 1L; + String accessToken = "mocked_access_token"; + String refreshToken = "mocked_refresh_token"; + + AuthInfoEntity entity = + AuthInfoEntity.builder().id(1L).memberId(userId).token(refreshToken).build(); + + when(tokenProvider.createAccessToken(userId)).thenReturn(accessToken); + when(tokenProvider.createRefreshToken(userId)).thenReturn(refreshToken); + when(authInfoEntityConverter.from(userId, refreshToken)).thenReturn(entity); + + // when + createTokenService.execute(userId); + + // then + verify(authInfoRepository).save(entity); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/ReissueServiceTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/ReissueServiceTest.java new file mode 100644 index 00000000..1644d11b --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/application/service/ReissueServiceTest.java @@ -0,0 +1,70 @@ +package com.blackcompany.eeos.auth.application.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.domain.token.TokenResolver; +import com.blackcompany.eeos.auth.application.exception.InvalidTokenException; +import com.blackcompany.eeos.auth.fixture.FakeAuthInfo; +import com.blackcompany.eeos.auth.persistence.AuthInfoEntity; +import com.blackcompany.eeos.auth.persistence.AuthInfoRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReissueServiceTest { + + @Mock CreateTokenService createTokenService; + @Mock AuthInfoRepository authInfoRepository; + + @Mock TokenResolver tokenResolver; + @InjectMocks ReissueService reissueService; + + @Test + @DisplayName("전달받은 토큰이 서버가 가지고 있는 유저의 토큰이 아닐 때 유효하지 않은 토큰을 제거하고 예외를 발생시킨다.") + void exception_when_token_invalid() { + // given + String token = "token"; + Long memberId = 1L; + + AuthInfoEntity authInfoEntity = FakeAuthInfo.authInfoEntity(); + + when(tokenResolver.getUserInfo(token)).thenReturn(memberId); + when(authInfoRepository.findByMemberIdAndToken(memberId, token)) + .thenReturn(Optional.ofNullable(null)); + when(authInfoRepository.findByToken(token)).thenReturn(Optional.ofNullable(authInfoEntity)); + + // when & then + assertAll( + () -> assertThrows(InvalidTokenException.class, () -> reissueService.execute(token)), + () -> verify(authInfoRepository).delete(authInfoEntity)); + } + + @Test + @DisplayName("전달받은 토큰이 유효한 토큰이라면 해당 토큰 정보를 제거하고 새로운 토큰을 생성한다.") + void execute() { + // given + String token = "token"; + Long memberId = 1L; + + AuthInfoEntity authInfoEntity = FakeAuthInfo.authInfoEntity(); + + when(tokenResolver.getUserInfo(token)).thenReturn(memberId); + when(authInfoRepository.findByMemberIdAndToken(memberId, token)) + .thenReturn(Optional.ofNullable(authInfoEntity)); + + // when + reissueService.execute(token); + + // then + assertAll( + () -> verify(authInfoRepository).delete(authInfoEntity), + () -> verify(createTokenService).execute(memberId)); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeAuthInfo.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeAuthInfo.java new file mode 100644 index 00000000..b0946001 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeAuthInfo.java @@ -0,0 +1,9 @@ +package com.blackcompany.eeos.auth.fixture; + +import com.blackcompany.eeos.auth.persistence.AuthInfoEntity; + +public class FakeAuthInfo { + public static AuthInfoEntity authInfoEntity() { + return AuthInfoEntity.builder().memberId(1L).token("token").build(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeMember.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeMember.java new file mode 100644 index 00000000..59807754 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeMember.java @@ -0,0 +1,14 @@ +package com.blackcompany.eeos.auth.fixture; + +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import com.blackcompany.eeos.member.persistence.MemberEntity; + +public class FakeMember { + public static MemberEntity memberEntity() { + return MemberEntity.builder() + .id(1L) + .name("name") + .oauthServerType(OauthServerType.SLACK) + .build(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMember.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMember.java new file mode 100644 index 00000000..c2774137 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMember.java @@ -0,0 +1,19 @@ +package com.blackcompany.eeos.auth.fixture; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import com.blackcompany.eeos.auth.persistence.OauthInfoEntity; + +public class FakeOauthMember { + public static OauthMemberModel oauthMemberModel() { + return OauthMemberModel.builder() + .oauthId("oauthId") + .name("name") + .oauthServerType(OauthServerType.SLACK) + .build(); + } + + public static OauthInfoEntity oauthInfoEntity() { + return OauthInfoEntity.builder().oauthId("oauthId").memberId(1L).build(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMemberClient.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMemberClient.java new file mode 100644 index 00000000..7cec73bd --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeOauthMemberClient.java @@ -0,0 +1,20 @@ +package com.blackcompany.eeos.auth.fixture; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import com.blackcompany.eeos.auth.application.domain.client.OauthMemberClient; + +public class FakeOauthMemberClient implements OauthMemberClient { + @Override + public OauthServerType support() { + return OauthServerType.SLACK; + } + + @Override + public OauthMemberModel fetch(String code) { + return OauthMemberModel.builder() + .oauthId("oauth_id") + .oauthServerType(OauthServerType.SLACK) + .build(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeSlackApiClientFixture.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeSlackApiClientFixture.java new file mode 100644 index 00000000..5851397c --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/fixture/FakeSlackApiClientFixture.java @@ -0,0 +1,44 @@ +package com.blackcompany.eeos.auth.fixture; + +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember.UserProfile; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken.AuthedUser; + +public class FakeSlackApiClientFixture { + private static final String userId = "userId"; + private static final String token = "token"; + private static final String name = "oauth_name"; + + public static SlackToken successSlackToken() { + return SlackToken.builder() + .ok(true) + .authedUser(AuthedUser.builder().userId(userId).accessToken(token).build()) + .error("") + .build(); + } + + public SlackToken failSlackToken(String client, String code, String clientSecret) { + return SlackToken.builder() + .ok(false) + .authedUser(AuthedUser.builder().userId("userId").accessToken("access_token").build()) + .error("error_message") + .build(); + } + + public SlackMember failSlackMember(String token) { + return SlackMember.builder() + .ok(false) + .profile(UserProfile.builder().displayName("oauth_name").build()) + .error("error_message") + .build(); + } + + public static SlackMember successSlackMember() { + return SlackMember.builder() + .ok(true) + .profile(UserProfile.builder().displayName(name).build()) + .error("") + .build(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/FakeSlackApiClient.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/FakeSlackApiClient.java new file mode 100644 index 00000000..2a416754 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/FakeSlackApiClient.java @@ -0,0 +1,18 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.client; + +import com.blackcompany.eeos.auth.fixture.FakeSlackApiClientFixture; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackMember; +import com.blackcompany.eeos.auth.infra.oauth.slack.dto.SlackToken; + +class FakeSlackApiClient implements SlackApiClient { + + @Override + public SlackToken fetchToken(String client, String code, String clientSecret) { + return FakeSlackApiClientFixture.successSlackToken(); + } + + @Override + public SlackMember fetchMember(String token) { + return FakeSlackApiClientFixture.successSlackMember(); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClientTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClientTest.java new file mode 100644 index 00000000..6755c502 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/infra/oauth/slack/client/SlackOauthMemberClientTest.java @@ -0,0 +1,41 @@ +package com.blackcompany.eeos.auth.infra.oauth.slack.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.blackcompany.eeos.auth.application.domain.OauthMemberModel; +import com.blackcompany.eeos.auth.application.domain.OauthServerType; +import com.blackcompany.eeos.auth.fixture.FakeSlackApiClientFixture; +import com.blackcompany.eeos.auth.infra.oauth.slack.config.SlackOauthConfig; +import com.blackcompany.eeos.auth.infra.oauth.slack.converter.OauthModelConverter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = {SlackOauthConfig.class, OauthModelConverter.class}) +class SlackOauthMemberClientTest { + SlackOauthMemberClient slackOauthMemberClient; + + @BeforeEach + public void beforeEach() { + SlackOauthConfig slackOauthConfig = new SlackOauthConfig(); + slackOauthConfig.setClientId("client_id"); + slackOauthConfig.setClientSecret("client_secret"); + + OauthModelConverter oauthModelConverter = new OauthModelConverter(); + slackOauthMemberClient = + new SlackOauthMemberClient(slackOauthConfig, new FakeSlackApiClient(), oauthModelConverter); + } + + @Test + @DisplayName("슬랙 api에 요청하여 슬랙 정보를 가져온다.") + void fetch() { + // when + OauthMemberModel model = slackOauthMemberClient.fetch("code"); + + // then + assertEquals(model.getOauthId(), FakeSlackApiClientFixture.successSlackToken().getUserId()); + assertEquals(model.getOauthServerType(), OauthServerType.SLACK); + assertEquals(model.getName(), FakeSlackApiClientFixture.successSlackMember().getName()); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractorTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractorTest.java new file mode 100644 index 00000000..26bf1cf5 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/CookieTokenExtractorTest.java @@ -0,0 +1,69 @@ +package com.blackcompany.eeos.auth.presentation.support; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.exception.NotFoundCookieException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CookieTokenExtractorTest { + @Mock HttpServletRequest request; + CookieTokenExtractor cookieTokenExtractor; + + @BeforeEach + void setUp() { + cookieTokenExtractor = new CookieTokenExtractor(); + } + + @Test + @DisplayName("요청에 쿠키가 없으면 예외가 발생한다.") + void not_found_cookie_exception() { + // given + when(request.getCookies()).thenReturn(null); + + // when & then + assertThrows(NotFoundCookieException.class, () -> cookieTokenExtractor.extract(request)); + } + + @Test + @DisplayName("토큰을 키로 가지고 있는 쿠키가 없을 때 예외가 발생한다.") + void not_found_token_cookie_exception() { + // given + Cookie[] cookies = new Cookie[1]; + Cookie cookie = new Cookie("key", "value"); + cookies[0] = cookie; + + when(request.getCookies()).thenReturn(cookies); + + // when & then + assertThrows(NotFoundCookieException.class, () -> cookieTokenExtractor.extract(request)); + } + + @Test + @DisplayName("토큰을 키로 가지고 있는 쿠키가 있을 때 쿠키의 값을 반환한다.") + void existing_token_cookie() { + // given + String key = "token"; + String value = "value"; + + Cookie[] cookies = new Cookie[1]; + Cookie cookie = new Cookie(key, value); + cookies[0] = cookie; + + when(request.getCookies()).thenReturn(cookies); + + // when + String extract = cookieTokenExtractor.extract(request); + + // then + assertEquals(value, extract); + } +} diff --git a/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractorTest.java b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractorTest.java new file mode 100644 index 00000000..f36f3a87 --- /dev/null +++ b/BE/eeos/src/test/java/com/blackcompany/eeos/auth/presentation/support/HeaderTokenExtractorTest.java @@ -0,0 +1,63 @@ +package com.blackcompany.eeos.auth.presentation.support; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +import com.blackcompany.eeos.auth.application.exception.NotFoundHeaderTokenException; +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HeaderTokenExtractorTest { + public static final String AUTHORIZATION = "Authorization"; + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + + @Mock HttpServletRequest request; + HeaderTokenExtractor headerTokenExtractor; + + @BeforeEach + void setUp() { + headerTokenExtractor = new HeaderTokenExtractor(); + } + + @Test + @DisplayName("인증 헤더가 존재하지 않으면 예외가 발생한다.") + void not_found_cookie_exception() { + // given + Mockito.when(request.getHeader(AUTHORIZATION)).thenReturn(null); + + // when & then + assertThrows(NotFoundHeaderTokenException.class, () -> headerTokenExtractor.extract(request)); + } + + @Test + @DisplayName("인증 헤더의 값에 prefix가 존재하지 않으면 예외가 발생한다.") + void not_found_token_cookie_exception() { + String value = "value"; + // given + when(request.getHeader(AUTHORIZATION)).thenReturn(value); + + // when & then + assertThrows(NotFoundHeaderTokenException.class, () -> headerTokenExtractor.extract(request)); + } + + @Test + @DisplayName("인증 헤더에 값이 존재할 때 prefix를 제외한 헤더의 값을 반환한다.") + void existing_token_cookie() { + // given + String value = "value"; + when(request.getHeader(AUTHORIZATION)).thenReturn(BEARER_TOKEN_PREFIX + value); + + // when + String extract = headerTokenExtractor.extract(request); + + // then + assertEquals(value, extract); + } +} diff --git a/BE/eeos/src/test/resources/application-test-mysql.yml b/BE/eeos/src/test/resources/application-test-mysql.yml index 6c85e4c2..dd32913b 100644 --- a/BE/eeos/src/test/resources/application-test-mysql.yml +++ b/BE/eeos/src/test/resources/application-test-mysql.yml @@ -9,7 +9,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: validate + ddl-auto: create properties: hibernate: format_sql: true diff --git a/BE/eeos/src/test/resources/application-test-token.yml b/BE/eeos/src/test/resources/application-test-token.yml new file mode 100644 index 00000000..e33335c6 --- /dev/null +++ b/BE/eeos/src/test/resources/application-test-token.yml @@ -0,0 +1,13 @@ +spring: + config: + activate: + on-profile: test-token + +security: + jwt: + token: + secretKey: asccesssecretkeyoverflowsecrekey + access: + validTime: 1800000 + refresh: + validTime: 3600000 \ No newline at end of file diff --git a/BE/eeos/src/test/resources/application.yml b/BE/eeos/src/test/resources/application.yml index 2c2d059e..95caf45c 100644 --- a/BE/eeos/src/test/resources/application.yml +++ b/BE/eeos/src/test/resources/application.yml @@ -4,4 +4,6 @@ spring: test: - test-mysql - test-api + - test-token + - oauth active: test \ No newline at end of file