From 7dcd36bcbc396755ba4fdc5696e59c86aa1545af Mon Sep 17 00:00:00 2001 From: fuse Date: Sun, 6 Aug 2023 17:34:54 +0900 Subject: [PATCH 01/87] =?UTF-8?q?#chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d9f71a0c4..7a898aa46 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,9 +36,6 @@ function App() { return ( - {/* - Change Theme - */} - } /> + } /> } From dfa93bc7b92d0f0209245ff4c35b503649d82a22 Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Mon, 7 Aug 2023 14:26:37 +0900 Subject: [PATCH 02/87] =?UTF-8?q?#25=20refactor=20:=20user=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20userValidator=20=EC=83=9D=EC=84=B1=20-?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 12 -- .../issueTracker/comment/domain/Comment.java | 26 --- .../comment/repository/CommentRepository.java | 13 -- .../comment/service/CommentService.java | 12 -- .../global/config/OauthConfig.java | 24 +-- .../issue/controller/IssueController.java | 12 -- .../issueTracker/issue/domain/Issue.java | 30 ---- .../issue/repository/IssueRepository.java | 13 -- .../issue/service/IssueService.java | 12 -- .../jwt/domain/OAuthProperties.java | 32 ---- .../jwt/domain/OauthAttributes.java | 35 ---- .../jwt/util/InMemoryProviderRepository.java | 10 +- .../issueTracker/jwt/util/OauthAdapter.java | 19 -- .../issueTracker/jwt/util/OauthProvider.java | 28 --- .../label/controller/LabelController.java | 12 -- .../issueTracker/label/domain/Label.java | 22 --- .../label/repository/LabelRepository.java | 13 -- .../label/service/LabelService.java | 12 -- .../controller/MilestoneController.java | 13 -- .../milestone/domain/Milestone.java | 24 --- .../repository/MilestoneRepository.java | 13 -- .../milestone/service/MilestoneService.java | 12 -- .../oauth/domain/OAuthProperties.java | 32 ++++ .../oauth/domain/OauthAttributes.java | 37 ++++ .../oauth/service/OAuthService.java | 116 +++++++++++++ .../issueTracker/oauth/util/OauthAdapter.java | 20 +++ .../oauth/util/OauthProvider.java | 29 ++++ .../user/controller/UserController.java | 4 +- .../issueTracker/user/domain/User.java | 15 ++ .../user/service/UserService.java | 164 ++++-------------- .../user/service/UserValidator.java | 29 ++++ .../user/service/UserServiceTest.java | 117 ------------- 32 files changed, 338 insertions(+), 624 deletions(-) delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/comment/controller/CommentController.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/comment/domain/Comment.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/comment/service/CommentService.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/issue/domain/Issue.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OAuthProperties.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OauthAttributes.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthAdapter.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthProvider.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/service/LabelService.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java delete mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OAuthProperties.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OauthAttributes.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/oauth/service/OAuthService.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthAdapter.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthProvider.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java delete mode 100644 be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java diff --git a/be/issue/src/main/java/codesquad/issueTracker/comment/controller/CommentController.java b/be/issue/src/main/java/codesquad/issueTracker/comment/controller/CommentController.java deleted file mode 100644 index b15037e9c..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/comment/controller/CommentController.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.comment.controller; - -import org.springframework.web.bind.annotation.RestController; - -import codesquad.issueTracker.comment.service.CommentService; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@RestController -public class CommentController { - private final CommentService commentService; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/comment/domain/Comment.java b/be/issue/src/main/java/codesquad/issueTracker/comment/domain/Comment.java deleted file mode 100644 index 3d0abcf28..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/comment/domain/Comment.java +++ /dev/null @@ -1,26 +0,0 @@ -package codesquad.issueTracker.comment.domain; - -import java.time.LocalDateTime; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class Comment { - - private Long id; - private Long userId; - private Long issueId; - private String content; - private LocalDateTime createdAt; - - @Builder - - public Comment(Long id, Long userId, Long issueId, String content, LocalDateTime createdAt) { - this.id = id; - this.userId = userId; - this.issueId = issueId; - this.content = content; - this.createdAt = createdAt; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java b/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java deleted file mode 100644 index 6f9356a2d..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/comment/repository/CommentRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package codesquad.issueTracker.comment.repository; - -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class CommentRepository { - private final NamedParameterJdbcTemplate jdbcTemplate; - - public CommentRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/comment/service/CommentService.java b/be/issue/src/main/java/codesquad/issueTracker/comment/service/CommentService.java deleted file mode 100644 index ac38d13ba..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/comment/service/CommentService.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.comment.service; - -import org.springframework.stereotype.Service; - -import codesquad.issueTracker.comment.repository.CommentRepository; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Service -public class CommentService { - private final CommentRepository commentRepository; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/config/OauthConfig.java b/be/issue/src/main/java/codesquad/issueTracker/global/config/OauthConfig.java index 2414a1361..f373a27c2 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/config/OauthConfig.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/config/OauthConfig.java @@ -1,25 +1,27 @@ package codesquad.issueTracker.global.config; -import codesquad.issueTracker.jwt.domain.OAuthProperties; -import codesquad.issueTracker.jwt.util.InMemoryProviderRepository; -import codesquad.issueTracker.jwt.util.OauthAdapter; -import codesquad.issueTracker.jwt.util.OauthProvider; import java.util.Map; -import lombok.RequiredArgsConstructor; + import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import codesquad.issueTracker.jwt.util.InMemoryProviderRepository; +import codesquad.issueTracker.oauth.domain.OAuthProperties; +import codesquad.issueTracker.oauth.util.OauthAdapter; +import codesquad.issueTracker.oauth.util.OauthProvider; +import lombok.RequiredArgsConstructor; + @Configuration @EnableConfigurationProperties(OAuthProperties.class) @RequiredArgsConstructor public class OauthConfig { - private final OAuthProperties properties; + private final OAuthProperties properties; - @Bean - public InMemoryProviderRepository inMemoryProviderRepository() { - Map providers = OauthAdapter.getOauthProviders(properties); - return new InMemoryProviderRepository(providers); - } + @Bean + public InMemoryProviderRepository inMemoryProviderRepository() { + Map providers = OauthAdapter.getOauthProviders(properties); + return new InMemoryProviderRepository(providers); + } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java b/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java deleted file mode 100644 index 515ec4042..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.issue.controller; - -import org.springframework.web.bind.annotation.RestController; - -import codesquad.issueTracker.issue.service.IssueService; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@RestController -public class IssueController { - private final IssueService issueService; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/domain/Issue.java b/be/issue/src/main/java/codesquad/issueTracker/issue/domain/Issue.java deleted file mode 100644 index 5fe40dc55..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/domain/Issue.java +++ /dev/null @@ -1,30 +0,0 @@ -package codesquad.issueTracker.issue.domain; - -import java.time.LocalDateTime; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class Issue { - - private Long id; - private Long milestoneId; - private Long userId; - private String title; - private String content; - private LocalDateTime createdAt; - private Boolean isClosed; - - @Builder - public Issue(Long id, Long milestoneId, Long userId, String title, String content, LocalDateTime createdAt, - Boolean isClosed) { - this.id = id; - this.milestoneId = milestoneId; - this.userId = userId; - this.title = title; - this.content = content; - this.createdAt = createdAt; - this.isClosed = isClosed; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java b/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java deleted file mode 100644 index 50d6559c5..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/repository/IssueRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package codesquad.issueTracker.issue.repository; - -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class IssueRepository { - private final NamedParameterJdbcTemplate jdbcTemplate; - - public IssueRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java b/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java deleted file mode 100644 index fb175b54c..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.issue.service; - -import org.springframework.stereotype.Service; - -import codesquad.issueTracker.issue.repository.IssueRepository; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Service -public class IssueService { - private final IssueRepository issueRepository; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OAuthProperties.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OAuthProperties.java deleted file mode 100644 index 79b68cf2e..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OAuthProperties.java +++ /dev/null @@ -1,32 +0,0 @@ -package codesquad.issueTracker.jwt.domain; - - -import java.util.HashMap; -import java.util.Map; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; - - -@Getter -@ConfigurationProperties(prefix = "oauth2") -public class OAuthProperties { - - private final Map client = new HashMap<>(); - private final Map provider = new HashMap(); - - @Getter - @Setter - public static class Client { - private String clientId; - private String clientSecret; - private String redirectUrl; - } - - @Getter - @Setter - public static class Provider { - private String tokenUrl; - private String userInfoUrl; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OauthAttributes.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OauthAttributes.java deleted file mode 100644 index 798b52673..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/domain/OauthAttributes.java +++ /dev/null @@ -1,35 +0,0 @@ -package codesquad.issueTracker.jwt.domain; - -import codesquad.issueTracker.global.exception.CustomException; -import codesquad.issueTracker.global.exception.ErrorCode; -import java.util.Arrays; -import java.util.Map; - -public enum OauthAttributes { - GITHUB("github") { - @Override - public UserProfile of(Map attributes) { - return UserProfile.builder() - .email((String) attributes.get("email")) - .name((String) attributes.get("login")) - .imageUrl((String) attributes.get("avatar_url")) - .build(); - } - }; - - private final String providerName; - - OauthAttributes(String name) { - this.providerName = name; - } - - public static UserProfile extract(String providerName, Map attributes) { - return Arrays.stream(values()) - .filter(provider -> providerName.equals(provider.providerName)) - .findFirst() - .orElseThrow(() -> new CustomException(ErrorCode.NOT_SUPPORTED_PROVIDER)) - .of(attributes); - } - - public abstract UserProfile of(Map attributes); -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/InMemoryProviderRepository.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/util/InMemoryProviderRepository.java index f138588be..33436ed43 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/InMemoryProviderRepository.java +++ b/be/issue/src/main/java/codesquad/issueTracker/jwt/util/InMemoryProviderRepository.java @@ -1,13 +1,15 @@ package codesquad.issueTracker.jwt.util; import java.util.Map; + +import codesquad.issueTracker.oauth.util.OauthProvider; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class InMemoryProviderRepository { - private final Map providers; + private final Map providers; - public OauthProvider findByProviderName(String name) { - return providers.get(name); - } + public OauthProvider findByProviderName(String name) { + return providers.get(name); + } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthAdapter.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthAdapter.java deleted file mode 100644 index b83a88139..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthAdapter.java +++ /dev/null @@ -1,19 +0,0 @@ -package codesquad.issueTracker.jwt.util; - -import codesquad.issueTracker.jwt.domain.OAuthProperties; -import java.util.HashMap; -import java.util.Map; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -public class OauthAdapter { - - public static Map getOauthProviders(OAuthProperties properties) { - Map oauthProvider = new HashMap<>(); - - properties.getClient().forEach( - (key, value) -> oauthProvider.put( - key, new OauthProvider(value, properties.getProvider().get(key)))); - return oauthProvider; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthProvider.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthProvider.java deleted file mode 100644 index 31edef645..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/util/OauthProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -package codesquad.issueTracker.jwt.util; - -import codesquad.issueTracker.jwt.domain.OAuthProperties; -import lombok.Builder; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -public class OauthProvider { - private final String clientId; - private final String clientSecret; - private final String redirectUrl; - private final String tokenUrl; - private final String userInfoUrl; - - public OauthProvider(OAuthProperties.Client client, OAuthProperties.Provider provider) { - this(client.getClientId(), client.getClientSecret(), client.getRedirectUrl(), provider.getTokenUrl(), provider.getUserInfoUrl()); - } - - @Builder - public OauthProvider(String clientId, String clientSecret, String redirectUrl, String tokenUrl, String userInfoUrl) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.redirectUrl = redirectUrl; - this.tokenUrl = tokenUrl; - this.userInfoUrl = userInfoUrl; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java deleted file mode 100644 index 8239ef7ba..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.label.controller; - -import org.springframework.web.bind.annotation.RestController; - -import codesquad.issueTracker.label.service.LabelService; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@RestController -public class LabelController { - private final LabelService labelService; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java b/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java deleted file mode 100644 index e2f8e98ba..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java +++ /dev/null @@ -1,22 +0,0 @@ -package codesquad.issueTracker.label.domain; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class Label { - private Long id; - private String name; - private String description; - private String backgroundColor; - private String textColor; - - @Builder - public Label(Long id, String name, String description, String backgroundColor, String textColor) { - this.id = id; - this.name = name; - this.description = description; - this.backgroundColor = backgroundColor; - this.textColor = textColor; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java b/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java deleted file mode 100644 index 0be9015fd..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package codesquad.issueTracker.label.repository; - -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class LabelRepository { - private final NamedParameterJdbcTemplate jdbcTemplate; - - public LabelRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/service/LabelService.java b/be/issue/src/main/java/codesquad/issueTracker/label/service/LabelService.java deleted file mode 100644 index cd084b4b6..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/label/service/LabelService.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.label.service; - -import org.springframework.stereotype.Service; - -import codesquad.issueTracker.label.repository.LabelRepository; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Service -public class LabelService { - private final LabelRepository labelRepository; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java deleted file mode 100644 index 8d4cc26cb..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java +++ /dev/null @@ -1,13 +0,0 @@ -package codesquad.issueTracker.milestone.controller; - -import org.springframework.web.bind.annotation.RestController; - -import codesquad.issueTracker.milestone.service.MilestoneService; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@RestController -public class MilestoneController { - private final MilestoneService milestoneService; - -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java deleted file mode 100644 index 4ff37378a..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java +++ /dev/null @@ -1,24 +0,0 @@ -package codesquad.issueTracker.milestone.domain; - -import java.time.LocalDateTime; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class Milestone { - private Long id; - private String name; - private String description; - private LocalDateTime doneDate; - private Boolean isClosed; - - @Builder - public Milestone(Long id, String name, String description, LocalDateTime doneDate, Boolean isClosed) { - this.id = id; - this.name = name; - this.description = description; - this.doneDate = doneDate; - this.isClosed = isClosed; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java deleted file mode 100644 index 1126f04cc..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package codesquad.issueTracker.milestone.repository; - -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.stereotype.Repository; - -@Repository -public class MilestoneRepository { - private final NamedParameterJdbcTemplate jdbcTemplate; - - public MilestoneRepository(NamedParameterJdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java deleted file mode 100644 index 78bb443b0..000000000 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java +++ /dev/null @@ -1,12 +0,0 @@ -package codesquad.issueTracker.milestone.service; - -import org.springframework.stereotype.Service; - -import codesquad.issueTracker.milestone.repository.MilestoneRepository; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Service -public class MilestoneService { - private final MilestoneRepository milestoneRepository; -} diff --git a/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OAuthProperties.java b/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OAuthProperties.java new file mode 100644 index 000000000..280070fa9 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OAuthProperties.java @@ -0,0 +1,32 @@ +package codesquad.issueTracker.oauth.domain; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@ConfigurationProperties(prefix = "oauth2") +public class OAuthProperties { + + private final Map client = new HashMap<>(); + private final Map provider = new HashMap(); + + @Getter + @Setter + public static class Client { + private String clientId; + private String clientSecret; + private String redirectUrl; + } + + @Getter + @Setter + public static class Provider { + private String tokenUrl; + private String userInfoUrl; + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OauthAttributes.java b/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OauthAttributes.java new file mode 100644 index 000000000..01143a9e5 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/oauth/domain/OauthAttributes.java @@ -0,0 +1,37 @@ +package codesquad.issueTracker.oauth.domain; + +import java.util.Arrays; +import java.util.Map; + +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.global.exception.ErrorCode; +import codesquad.issueTracker.jwt.domain.UserProfile; + +public enum OauthAttributes { + GITHUB("github") { + @Override + public UserProfile of(Map attributes) { + return UserProfile.builder() + .email((String)attributes.get("email")) + .name((String)attributes.get("login")) + .imageUrl((String)attributes.get("avatar_url")) + .build(); + } + }; + + private final String providerName; + + OauthAttributes(String name) { + this.providerName = name; + } + + public static UserProfile extract(String providerName, Map attributes) { + return Arrays.stream(values()) + .filter(provider -> providerName.equals(provider.providerName)) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.NOT_SUPPORTED_PROVIDER)) + .of(attributes); + } + + public abstract UserProfile of(Map attributes); +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/oauth/service/OAuthService.java b/be/issue/src/main/java/codesquad/issueTracker/oauth/service/OAuthService.java new file mode 100644 index 000000000..aad6d63e1 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/oauth/service/OAuthService.java @@ -0,0 +1,116 @@ +package codesquad.issueTracker.oauth.service; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.WebClient; + +import codesquad.issueTracker.jwt.domain.Jwt; +import codesquad.issueTracker.jwt.domain.UserProfile; +import codesquad.issueTracker.jwt.dto.OauthTokenResponse; +import codesquad.issueTracker.jwt.util.InMemoryProviderRepository; +import codesquad.issueTracker.jwt.util.JwtProvider; +import codesquad.issueTracker.oauth.domain.OauthAttributes; +import codesquad.issueTracker.oauth.util.OauthProvider; +import codesquad.issueTracker.user.domain.User; +import codesquad.issueTracker.user.dto.LoginResponseDto; +import codesquad.issueTracker.user.service.UserService; +import codesquad.issueTracker.user.service.UserValidator; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class OAuthService { + private final UserService userService; + private final UserValidator userValidator; + private final JwtProvider jwtProvider; + private final InMemoryProviderRepository inMemoryProviderRepository; + + @Transactional + public LoginResponseDto oauthLogin(String providerName, String code) { + OauthProvider provider = findByProviderName(providerName); + OauthTokenResponse tokenResponse = getToken(code, provider); + User user = getUserProfile(providerName, tokenResponse, provider).toUser(providerName); + + User findUser = userService.findExistedOrInsertedUser(user); + + userValidator.validateLoginType(user.getLoginType(), findUser.getLoginType()); + + Jwt jwt = jwtProvider.createJwt(Map.of("userId", findUser.getId())); + userService.insertOrUpdateToken(findUser.getId(), jwt); + + return LoginResponseDto.builder() + .userId(findUser.getId()) + .accessToken(jwt.getAccessToken()) + .refreshToken(jwt.getRefreshToken()) + .profileImgUrl(user.getProfileImg()) + .build(); + } + + public OauthTokenResponse getToken(String code, OauthProvider provider) { + return WebClient.create() + .post() + .uri(provider.getTokenUrl()) + .headers(header -> { + header.setBasicAuth(provider.getClientId(), provider.getClientSecret()); + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + header.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); + }) + .bodyValue(tokenRequest(code, provider)) + .retrieve() + .bodyToMono(OauthTokenResponse.class) + .block(); + } + + private MultiValueMap tokenRequest(String code, OauthProvider provider) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("code", code); + formData.add("grant_type", "authorization_code"); + formData.add("redirect_uri", provider.getRedirectUrl()); + return formData; + } + + public UserProfile getUserProfile(String providerName, OauthTokenResponse tokenResponse, OauthProvider provider) { + Map userAttributes = getUserAttributes(provider, tokenResponse); + if (providerName.equals("github") && userAttributes.get("email") == null) { + List> temp = getUserEmail(tokenResponse); + userAttributes.put("email", temp.get(0).get("email")); + } + return OauthAttributes.extract(providerName, userAttributes); + } + + public Map getUserAttributes(OauthProvider provider, OauthTokenResponse tokenResponse) { + return WebClient.create() + .get() + .uri(provider.getUserInfoUrl()) + .headers(header -> header.setBearerAuth(tokenResponse.getAccessToken())) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(); + } + + public List> getUserEmail(OauthTokenResponse tokenResponse) { + return WebClient.create() + .get() + .uri("https://api.github.com/user/emails") + .headers(header -> header.setBearerAuth(tokenResponse.getAccessToken())) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>>() { + }) + .block(); + } + + public OauthProvider findByProviderName(String name) { + return inMemoryProviderRepository.findByProviderName(name); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthAdapter.java b/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthAdapter.java new file mode 100644 index 000000000..c2a1252d5 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthAdapter.java @@ -0,0 +1,20 @@ +package codesquad.issueTracker.oauth.util; + +import java.util.HashMap; +import java.util.Map; + +import codesquad.issueTracker.oauth.domain.OAuthProperties; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class OauthAdapter { + + public static Map getOauthProviders(OAuthProperties properties) { + Map oauthProvider = new HashMap<>(); + + properties.getClient().forEach( + (key, value) -> oauthProvider.put( + key, new OauthProvider(value, properties.getProvider().get(key)))); + return oauthProvider; + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthProvider.java b/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthProvider.java new file mode 100644 index 000000000..33e265a81 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/oauth/util/OauthProvider.java @@ -0,0 +1,29 @@ +package codesquad.issueTracker.oauth.util; + +import codesquad.issueTracker.oauth.domain.OAuthProperties; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OauthProvider { + private final String clientId; + private final String clientSecret; + private final String redirectUrl; + private final String tokenUrl; + private final String userInfoUrl; + + public OauthProvider(OAuthProperties.Client client, OAuthProperties.Provider provider) { + this(client.getClientId(), client.getClientSecret(), client.getRedirectUrl(), provider.getTokenUrl(), + provider.getUserInfoUrl()); + } + + @Builder + public OauthProvider(String clientId, String clientSecret, String redirectUrl, String tokenUrl, + String userInfoUrl) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUrl = redirectUrl; + this.tokenUrl = tokenUrl; + this.userInfoUrl = userInfoUrl; + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java b/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java index ce81b169e..5a4b6a950 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java @@ -15,6 +15,7 @@ import codesquad.issueTracker.global.ApiResponse; import codesquad.issueTracker.jwt.dto.RequestRefreshTokenDto; +import codesquad.issueTracker.oauth.service.OAuthService; import codesquad.issueTracker.user.dto.LoginRequestDto; import codesquad.issueTracker.user.dto.LoginResponseDto; import codesquad.issueTracker.user.dto.SignUpRequestDto; @@ -26,6 +27,7 @@ @RequestMapping("/api") public class UserController { private final UserService userService; + private final OAuthService oAuthService; @PostMapping("/signup") public ApiResponse signUp(@Valid @RequestBody SignUpRequestDto userSignUpRequestDto) { @@ -55,7 +57,7 @@ public ApiResponse logout(HttpServletRequest request) { @GetMapping("/login/{provider}") public ApiResponse oauthLogin(@PathVariable String provider, @RequestParam String code) { - LoginResponseDto loginResponseDto = userService.oauthLogin(provider, code); + LoginResponseDto loginResponseDto = oAuthService.oauthLogin(provider, code); return ApiResponse.success(SUCCESS.getStatus(), loginResponseDto); } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java b/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java index 61af88138..0aa94bd62 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java @@ -1,5 +1,10 @@ package codesquad.issueTracker.user.domain; +import org.mindrot.jbcrypt.BCrypt; + +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.global.exception.ErrorCode; +import codesquad.issueTracker.user.dto.LoginRequestDto; import lombok.Builder; import lombok.Getter; @@ -24,4 +29,14 @@ public User(Long id, String email, String password, String profileImg, String na this.loginType = loginType; } + public void validateLoginUser(LoginRequestDto loginRequestDto) { + if (password == null) { + throw new CustomException(ErrorCode.GITHUB_LOGIN_USER); + } + if (!loginRequestDto.getEmail().equals(email) + || !BCrypt.checkpw(loginRequestDto.getPassword(), password)) { + throw new CustomException(ErrorCode.FAILED_LOGIN_USER); + } + } + } diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java index 4d162083f..92ebdc67d 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java @@ -1,21 +1,12 @@ package codesquad.issueTracker.user.service; -import codesquad.issueTracker.jwt.domain.OauthAttributes; -import codesquad.issueTracker.jwt.domain.UserProfile; -import codesquad.issueTracker.jwt.dto.OauthTokenResponse; -import codesquad.issueTracker.jwt.util.InMemoryProviderRepository; -import codesquad.issueTracker.jwt.util.OauthProvider; -import codesquad.issueTracker.user.domain.LoginType; -import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.Map; -import java.util.Collections; import javax.servlet.http.HttpServletRequest; + import org.mindrot.jbcrypt.BCrypt; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import codesquad.issueTracker.global.exception.CustomException; import codesquad.issueTracker.global.exception.ErrorCode; @@ -23,26 +14,25 @@ import codesquad.issueTracker.jwt.domain.Token; import codesquad.issueTracker.jwt.dto.RequestRefreshTokenDto; import codesquad.issueTracker.jwt.util.JwtProvider; +import codesquad.issueTracker.user.domain.LoginType; import codesquad.issueTracker.user.domain.User; import codesquad.issueTracker.user.dto.LoginRequestDto; import codesquad.issueTracker.user.dto.LoginResponseDto; import codesquad.issueTracker.user.dto.SignUpRequestDto; import codesquad.issueTracker.user.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.client.WebClient; @RequiredArgsConstructor @Service +@Transactional public class UserService { private final UserRepository userRepository; + private final UserValidator userValidator; private final JwtProvider jwtProvider; - private final InMemoryProviderRepository inMemoryProviderRepository; private final String DEFAULT_PROFILE_IMG = "https://upload.wikimedia.org/wikipedia/commons/1/17/Enhydra_lutris_face.jpg"; public Long save(SignUpRequestDto userSignUpRequestDto) { - validateDuplicatedEmail(userSignUpRequestDto); + userValidator.validateDuplicatedEmail(userSignUpRequestDto); String encodedPassword = BCrypt.hashpw(userSignUpRequestDto.getPassword(), BCrypt.gensalt()); User user = SignUpRequestDto.toEntity(userSignUpRequestDto, encodedPassword, DEFAULT_PROFILE_IMG); return userRepository.insert(user); @@ -58,137 +48,59 @@ public Long save(SignUpRequestDto userSignUpRequestDto) { */ public LoginResponseDto login(LoginRequestDto loginRequestDto) { User user = userRepository.findByEmail(loginRequestDto.getEmail()) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); - if(user.getPassword() == null){ - throw new CustomException(ErrorCode.GITHUB_LOGIN_USER); - } - validateLoginUser(loginRequestDto, user); - validateLoginType(LoginType.LOCAL, user.getLoginType()); - Jwt jwt = insertToken(user.getId()); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); + user.validateLoginUser(loginRequestDto); + + userValidator.validateLoginType(LoginType.LOCAL, user.getLoginType()); + + Jwt jwt = jwtProvider.createJwt(Map.of("userId", user.getId())); + + insertOrUpdateToken(user.getId(), jwt); + return LoginResponseDto.builder() - .userId(user.getId()) - .profileImgUrl(user.getProfileImg()) - .accessToken(jwt.getAccessToken()) - .refreshToken(jwt.getRefreshToken()) - .build(); + .userId(user.getId()) + .profileImgUrl(user.getProfileImg()) + .accessToken(jwt.getAccessToken()) + .refreshToken(jwt.getRefreshToken()) + .build(); } - public void validateLoginUser(LoginRequestDto loginRequestDto, User user) { - if (!loginRequestDto.getEmail().equals(user.getEmail()) - || !BCrypt.checkpw(loginRequestDto.getPassword(), user.getPassword())) - throw new CustomException(ErrorCode.FAILED_LOGIN_USER); + public void insertOrUpdateToken(Long userId, Jwt jwt) { + if (userRepository.findTokenByUserId(userId).isEmpty()) { + userRepository.insertRefreshToken(userId, jwt.getRefreshToken()); + } else { + userRepository.updateRefreshToken(userId, jwt.getRefreshToken()); + } } - public void validateDuplicatedEmail(SignUpRequestDto signUpRequestDto) { - if (!userRepository.findByEmail(signUpRequestDto.getEmail()).isEmpty()) { - throw new CustomException(ErrorCode.ALREADY_EXIST_USER); - } + private User insertOauthUser(User user) { + userRepository.insert(user); + return userRepository.findByEmail(user.getEmail()) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); + } + + public User findExistedOrInsertedUser(User user) { + return userRepository.findByEmail(user.getEmail()) + .orElseGet(() -> insertOauthUser(user)); } /** * 1. 만료된 리프레시 토큰이면 예외처리 * 2. DB에 없는 리프레시 토큰이면 예외처리 */ + @Transactional(readOnly = true) public String reissueAccessToken(RequestRefreshTokenDto refreshTokenDto) { jwtProvider.getClaims(refreshTokenDto.getRefreshToken()); Token token = userRepository.findTokenByUserToken(refreshTokenDto.getRefreshToken()) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_REFRESH_TOKEN)); + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_REFRESH_TOKEN)); return jwtProvider.reissueAccessToken(Map.of("userId", token.getUserId())); } public void logout(HttpServletRequest request) { Long userId = Long.parseLong(String.valueOf(request.getAttribute("userId"))); userRepository.deleteTokenByUserId(userId) - .orElseThrow(() -> new CustomException(ErrorCode.DELETE_FAIL)); - } - - public LoginResponseDto oauthLogin(String providerName, String code) { - OauthProvider provider = inMemoryProviderRepository.findByProviderName(providerName); - OauthTokenResponse tokenResponse = getToken(code, provider); - User user = getUserProfile(providerName, tokenResponse, provider).toUser(providerName); - User existUser =userRepository.findByEmail(user.getEmail()).orElseGet(() -> insertOauthUser(user)); - validateLoginType(user.getLoginType(), existUser.getLoginType()); - Jwt jwt = insertToken(existUser.getId()); - return LoginResponseDto.builder() - .userId(existUser.getId()) - .accessToken(jwt.getAccessToken()) - .refreshToken(jwt.getRefreshToken()) - .profileImgUrl(user.getProfileImg()) - .build(); - } - - private OauthTokenResponse getToken(String code, OauthProvider provider) { - return WebClient.create() - .post() - .uri(provider.getTokenUrl()) - .headers(header -> { - header.setBasicAuth(provider.getClientId(), provider.getClientSecret()); - header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - header.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); - }) - .bodyValue(tokenRequest(code,provider)) - .retrieve() - .bodyToMono(OauthTokenResponse.class) - .block(); + .orElseThrow(() -> new CustomException(ErrorCode.DELETE_FAIL)); } - private MultiValueMap tokenRequest(String code, OauthProvider provider) { - MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("code", code); - formData.add("grant_type", "authorization_code"); - formData.add("redirect_uri", provider.getRedirectUrl()); - return formData; - } - - private UserProfile getUserProfile(String providerName, OauthTokenResponse tokenResponse, OauthProvider provider) { - Map userAttributes = getUserAttributes(provider, tokenResponse); - if(providerName.equals("github") && userAttributes.get("email") == null) { - List> temp = getUserEmail(tokenResponse); - userAttributes.put("email",temp.get(0).get("email")); - } - return OauthAttributes.extract(providerName, userAttributes); - } - - private Map getUserAttributes(OauthProvider provider, OauthTokenResponse tokenResponse) { - return WebClient.create() - .get() - .uri(provider.getUserInfoUrl()) - .headers(header -> header.setBearerAuth(tokenResponse.getAccessToken())) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}) - .block(); - } - private Jwt insertToken(Long userId){ - Jwt jwt = jwtProvider.createJwt(Map.of("userId", userId)); - if(userRepository.findTokenByUserId(userId).isEmpty()){ - userRepository.insertRefreshToken(userId,jwt.getRefreshToken()); - } - else { - userRepository.updateRefreshToken(userId, jwt.getRefreshToken()); - } - return jwt; - } - - private void validateLoginType(LoginType inputLoginType, LoginType existLoginType){ - if(inputLoginType != existLoginType){ - throw new CustomException(ErrorCode.FAILED_LOGIN_USER); - } - } - - private List> getUserEmail(OauthTokenResponse tokenResponse){ - return WebClient.create() - .get() - .uri("https://api.github.com/user/emails") - .headers(header -> header.setBearerAuth(tokenResponse.getAccessToken())) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() {}) - .block(); - } - - private User insertOauthUser(User user){ - userRepository.insert(user); - return userRepository.findByEmail(user.getEmail()).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); - } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java new file mode 100644 index 000000000..7c8927af6 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java @@ -0,0 +1,29 @@ +package codesquad.issueTracker.user.service; + +import org.springframework.stereotype.Service; + +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.global.exception.ErrorCode; +import codesquad.issueTracker.user.domain.LoginType; +import codesquad.issueTracker.user.dto.SignUpRequestDto; +import codesquad.issueTracker.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class UserValidator { + private final UserRepository userRepository; + + public void validateLoginType(LoginType inputLoginType, LoginType existLoginType) { + if (inputLoginType != existLoginType) { + throw new CustomException(ErrorCode.FAILED_LOGIN_USER); + } + } + + public void validateDuplicatedEmail(SignUpRequestDto signUpRequestDto) { + if (!userRepository.findByEmail(signUpRequestDto.getEmail()).isEmpty()) { + throw new CustomException(ErrorCode.ALREADY_EXIST_USER); + } + } + +} diff --git a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java deleted file mode 100644 index f555853a0..000000000 --- a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package codesquad.issueTracker.user.service; - -import static org.assertj.core.api.AssertionsForClassTypes.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import java.util.Map; -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mindrot.jbcrypt.BCrypt; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import annotation.ServiceTest; -import codesquad.issueTracker.global.exception.CustomException; -import codesquad.issueTracker.jwt.domain.Jwt; -import codesquad.issueTracker.jwt.domain.Token; -import codesquad.issueTracker.jwt.util.JwtProvider; -import codesquad.issueTracker.user.domain.User; -import codesquad.issueTracker.user.dto.LoginRequestDto; -import codesquad.issueTracker.user.repository.UserRepository; - -@ServiceTest -class UserServiceTest { - @InjectMocks - private UserService userService; - - @Mock - private UserRepository userRepository; - - // private final JwtProvider jwtProvider = new JwtProvider(new JwtProperties()); - - @Mock - private JwtProvider jwtProvider; - - @Test - void save() { - - } - - @Test - void login() { - // given - User user = User.builder() - .id(1L) - .email("asd123@ddd.com") - .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) - .build(); - given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); - - Token token = Token.builder() - .id(1L) - .userId(1L) - .refreshToken("abc.def.ghi") - .build(); - given(userRepository.findTokenByUserId(1L)).willReturn(Optional.of(token)); - - given(userRepository.updateRefreshToken(any(), any())).willReturn(1); - - User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); - Token findToken = userRepository.findTokenByUserId(1L).orElseThrow(); - - given(jwtProvider.createJwt(any())).willReturn(new Jwt("accessToken", "abc.ddd.ggg")); - Jwt jwt = jwtProvider.createJwt(Map.of("userId", findUser.getId())); - - LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@ddd.com", "12345678"); - userService.validateLoginUser(loginRequestDto, findUser); - int result = userRepository.updateRefreshToken(1L, jwt.getRefreshToken()); - - assertThat(result).isEqualTo(1); - - } - - @DisplayName("로그인 성공 : request 의 email이 db에 findUser의 이메일과 일치하고 request의 pw 가 복호화된 findUser의 pw가 같을때 ") - @Test - void validateLoginUserSuccess() { - // given - User user = User.builder() - .id(1L) - .email("asd123@ddd.com") - .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) - .build(); - - given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); - User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); - LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@ddd.com", "12345678"); - // when - - assertDoesNotThrow(() -> userService.validateLoginUser(loginRequestDto, findUser)); - } - - @DisplayName("로그인 실패 : request 의 email이 db에 findUser의 이메일과 일치하지 않거나 request의 pw 가 복호화된 findUser의 pw가 같지 않을때 예외발생 ") - @Test - void validateLoginUserFailed() { - - User user = User.builder() - .id(1L) - .email("asdfff123@ddd.com") - .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) - .build(); - - given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); - User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); - LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@ddd.com", "123dd45678"); - - assertThrows(CustomException.class, () -> { - userService.validateLoginUser(loginRequestDto, findUser); - }); - } - - @Test - void validateDuplicatedEmail() { - - } -} \ No newline at end of file From db8503bb1d8351bfe36f4b1f2c13b50acc02036e Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Mon, 7 Aug 2023 14:27:14 +0900 Subject: [PATCH 03/87] =?UTF-8?q?#25=20test=20:=20UserValidator=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerTest.java | 65 +++++++++ .../user/service/UserValidatorTest.java | 136 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 be/issue/src/test/java/codesquad/issueTracker/user/controller/UserControllerTest.java create mode 100644 be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java diff --git a/be/issue/src/test/java/codesquad/issueTracker/user/controller/UserControllerTest.java b/be/issue/src/test/java/codesquad/issueTracker/user/controller/UserControllerTest.java new file mode 100644 index 000000000..e2fef9154 --- /dev/null +++ b/be/issue/src/test/java/codesquad/issueTracker/user/controller/UserControllerTest.java @@ -0,0 +1,65 @@ +package codesquad.issueTracker.user.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import codesquad.issueTracker.user.dto.LoginRequestDto; +import codesquad.issueTracker.user.dto.SignUpRequestDto; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Sql(scripts = {"classpath:schema/schema.sql"}) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + public void setMockMvc() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + + SignUpRequestDto signUpRequestDto = new SignUpRequestDto("asd123@ddd.com", "12345678", "sadfadsf"); + String request = objectMapper.writeValueAsString(signUpRequestDto); + mockMvc.perform(post("/api/signup") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(request)); + } + + @Test + void loginTest() throws Exception { + LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@ddd.com", "12345678"); + + String request = objectMapper.writeValueAsString(loginRequestDto); + + ResultActions resultActions = mockMvc.perform(post("/api/login") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(request)); + + // System.out.println(jsonPath("$[0].message.accessToken").exists()); + resultActions.andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.message.accessToken").exists()); + } +} \ No newline at end of file diff --git a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java new file mode 100644 index 000000000..7d3ddcc56 --- /dev/null +++ b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java @@ -0,0 +1,136 @@ +package codesquad.issueTracker.user.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mindrot.jbcrypt.BCrypt; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import annotation.ServiceTest; +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.user.domain.LoginType; +import codesquad.issueTracker.user.domain.User; +import codesquad.issueTracker.user.dto.LoginRequestDto; +import codesquad.issueTracker.user.dto.SignUpRequestDto; +import codesquad.issueTracker.user.repository.UserRepository; + +@ServiceTest +public class UserValidatorTest { + @InjectMocks + UserValidator userValidator; + @Mock + UserRepository userRepository; + + @DisplayName("로그인 성공 : request 의 email이 db에 findUser의 이메일과 일치하고 request의 pw 가 복호화된 findUser의 pw가 같을때 ") + @Test + void validateLoginUserSuccess() { + // given + User user = User.builder() + .id(1L) + .email("asd123@ddd.com") + .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) + .build(); + + given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); + User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); + LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@ddd.com", "12345678"); + // when + + assertDoesNotThrow(() -> findUser.validateLoginUser(loginRequestDto)); + } + + @DisplayName("로그인 실패 : request 의 email이 db에 findUser의 이메일과 일치하지 않거나 request의 pw 가 복호화된 findUser의 pw가 같지 않을때 예외발생 ") + @Test + void validateLoginUserFailed() { + + User user = User.builder() + .id(1L) + .email("asdfff123@ddd.com") + .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) + .build(); + + given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); + User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); + LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@dddd.com", "123dd45678"); + + assertThrows(CustomException.class, () -> { + findUser.validateLoginUser(loginRequestDto); + }); + } + + @DisplayName("로그인 실패 : 로그인 타입이 같으면 로그인 실패 예외발생하지 않음") + @Test + public void validateLoginTypeSuccess() { + // given + LoginType input = LoginType.LOCAL; + User existUser = User.builder() + .id(1L) + .email("asd123@ddd.com") + .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); + LoginType existUserLoginType = userRepository.findByEmail(existUser.getEmail()).get().getLoginType(); + assertDoesNotThrow(() -> { + // userValidator.validateLoginType(input, existUserLoginType); + }); + + } + + @DisplayName("로그인 실패 : 로그인 타입이 다르면 로그인 실패 예외 발생") + @Test + public void validateLoginTypeFailed() { + // given + LoginType input = LoginType.GITHUB; + User existUser = User.builder() + .id(1L) + .email("asd123@ddd.com") + .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); + LoginType existUserLoginType = userRepository.findByEmail(existUser.getEmail()).get().getLoginType(); + assertThrows(CustomException.class, () -> { + // userValidator.validateLoginType(input, existUserLoginType); + }); + + } + + @DisplayName("회원가입 실패 : email로 user 를 찾을 때 반환값이 없는 경우 예외발생하지 않음 ") + @Test + public void validateDuplicatedEmailSuccess() { + SignUpRequestDto signUpRequestDto = new SignUpRequestDto("asdd1d23@ddd.com", "12345678", "감귤감귤감귤"); + User existUser = User.builder() + .id(1L) + .email("asd1d2dfsa3@ddd.com") + .password(BCrypt.hashpw(signUpRequestDto.getPassword(), BCrypt.gensalt())) + .build(); + given(userRepository.findByEmail(any())).willReturn(Optional.empty()); + + assertDoesNotThrow(() -> { + // userValidator.validateDuplicatedEmail(signUpRequestDto); + }); + } + + @DisplayName("회원가입 실패 : 중복된 이메일 입력할 경우 예외발생 ") + @Test + public void validateDuplicatedEmailFailed() { + SignUpRequestDto signUpRequestDto = new SignUpRequestDto("asd123@ddd.com", "12345678", "감귤감귤감귤"); + User existUser = User.builder() + .id(1L) + .email(signUpRequestDto.getEmail()) + .password(BCrypt.hashpw(signUpRequestDto.getPassword(), BCrypt.gensalt())) + .build(); + given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); + + assertThrows(CustomException.class, () -> { + // userValidator.validateDuplicatedEmail(signUpRequestDto); + }); + } + +} From af3acb28444024a5257e73635b39e0217adb0ddd Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Mon, 7 Aug 2023 14:28:20 +0900 Subject: [PATCH 04/87] =?UTF-8?q?#25=20chore=20:=20filter=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codesquad/issueTracker/global/filter/AuthFilter.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/filter/AuthFilter.java b/be/issue/src/main/java/codesquad/issueTracker/global/filter/AuthFilter.java index 0e8db0097..52387ef8a 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/filter/AuthFilter.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/filter/AuthFilter.java @@ -42,9 +42,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpServletResponse httpServletResponse = (HttpServletResponse)response; - // if (httpServletRequest.getMethod().equals("GET")) { - // return; - // } if (whiteListCheck(httpServletRequest.getRequestURI())) { chain.doFilter(request, response); return; From a1349ce7bf5b4e8f239092dbaa5d44d7cb264d7d Mon Sep 17 00:00:00 2001 From: hyeseon Date: Mon, 7 Aug 2023 17:15:31 +0900 Subject: [PATCH 05/87] #13 ci: Create gradle.yml --- .github/workflows/gradle.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/gradle.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 000000000..ee2544614 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,32 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: release + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + with: + arguments: build From db2faf9720317a7770773c4ddebb752fa2b51df2 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Mon, 7 Aug 2023 18:18:43 +0900 Subject: [PATCH 06/87] =?UTF-8?q?#13=20ci:=20appspec=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20gradle.yml=20->=20deploy.yml=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/{gradle.yml => deploy.yml} | 0 .gitignore | 1 - appspec.yml | 19 +++++++++++++++++++ be/issue/.gitignore | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) rename .github/workflows/{gradle.yml => deploy.yml} (100%) create mode 100644 appspec.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/deploy.yml similarity index 100% rename from .github/workflows/gradle.yml rename to .github/workflows/deploy.yml diff --git a/.gitignore b/.gitignore index 06b59439a..95f3a3dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,3 @@ dist /.idea/compiler.xml /.idea/codeStyles/codeStyleConfig.xml /.idea/checkstyle-idea.xml -*.yml \ No newline at end of file diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 000000000..9e24388bf --- /dev/null +++ b/appspec.yml @@ -0,0 +1,19 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/app + overwrite: yes + +permissions: + - object: / + pattern: "**" + owner: ubuntu + group: ubuntu + +hooks: + AfterInstall: + - location: deploy-app.sh + timeout: 60 + runas: ubuntu diff --git a/be/issue/.gitignore b/be/issue/.gitignore index c2065bc26..649136dfb 100644 --- a/be/issue/.gitignore +++ b/be/issue/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +*.yml From e63d51fbd2580bcbc7508420a6555f070c23dfcc Mon Sep 17 00:00:00 2001 From: hyeseon Date: Mon, 7 Aug 2023 23:57:04 +0900 Subject: [PATCH 07/87] =?UTF-8?q?#13=20ci:=20appspec,=20deploy=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 74 +++++++++++++++++++++++++++--------- scripts/deploy-app.sh | 12 ++++++ 2 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 scripts/deploy-app.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ee2544614..94964e593 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,32 +1,68 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle - name: Java CI with Gradle on: push: branches: release +env: + working-directory: ./be + AWS_REGION: ap-northeast-2 + S3_BUCKET_NAME: issue-tracker-s3-bucket + CODE_DEPLOY_APPLICATION_NAME: issue-tracker-codedeploy-app + CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: issuetracker-codedeploy-deployment-group + permissions: contents: read jobs: - build: - + app-build-and-deploy: + name: BE Deploy runs-on: ubuntu-latest + environment: production + defaults: + run: + shell: bash + working-directory: ./be steps: - - uses: actions/checkout@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'temurin' - - name: Build with Gradle - uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 - with: - arguments: build + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + + - uses: actions/checkout@v3 + - run: touch -c application.yml + - run: echo "${{ secrets.APPLICATION }}" > application.yml + - run: mv application.yml be/issue/src/main/resources/application.yml + + - name: Build with Gradle + run: | + cd be/issue + chmod +x gradlew + ./gradlew clean build -x test + + - name: Make zip file + run: zip -r ./$GITHUB_SHA.zip . + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}} + aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}} + aws-region: ${{env.AWS_REGION}} + + - name: Upload to S3 + run: aws s3 cp + --region '${{ env.AWS_REGION }}' ./$GITHUB_SHA.zip + s3://$S3_BUCKET_NAME/Build/$GITHUB_SHA.zip + + - name: Code Deploy + run: aws deploy create-deployment + --application-name $CODE_DEPLOY_APPLICATION_NAME + --deployment-config-name CodeDeployDefault.AllAtOnce + --deployment-group-name $CODE_DEPLOY_DEPLOYMENT_GROUP_NAME + --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=Build/$GITHUB_SHA.zip diff --git a/scripts/deploy-app.sh b/scripts/deploy-app.sh new file mode 100644 index 000000000..2a09b14a5 --- /dev/null +++ b/scripts/deploy-app.sh @@ -0,0 +1,12 @@ +KIOSK_ID=$(jps | grep be | awk '{ print $1 }') + +if [ -z $KIOSK_ID ]; then + echo "동작중인 서버가 없습니다." +else + echo "$KIOSK_ID 프로세스를 삭제합니다." + kill -9 $KIOSK_ID +fi + +echo "서버 시작" +nohup java -jar ~/app/issue-0.0.1-SNAPSHOT.jar >/home/ubuntu/app/log.txt 2>&1 & +echo "배포 성공" \ No newline at end of file From 6257d69d0afa308581abc1a6cb0f783d0a7c7da1 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 00:06:26 +0900 Subject: [PATCH 08/87] =?UTF-8?q?#13=20ci:=20application=20yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 94964e593..9523ab203 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,7 @@ on: env: working-directory: ./be + APPLICATION_YML_FILE_PATH: ./src/main/resources/application.yml AWS_REGION: ap-northeast-2 S3_BUCKET_NAME: issue-tracker-s3-bucket CODE_DEPLOY_APPLICATION_NAME: issue-tracker-codedeploy-app @@ -34,10 +35,8 @@ jobs: java-version: '11' distribution: 'temurin' - - uses: actions/checkout@v3 - - run: touch -c application.yml - - run: echo "${{ secrets.APPLICATION }}" > application.yml - - run: mv application.yml be/issue/src/main/resources/application.yml + - name: Output application information + run: echo '${{ secrets.APPLICATION }}' > '${{ env.APPLICATION_YML_FILE_PATH }}' - name: Build with Gradle run: | From e62b1428b0f87ee420cf57d70c593b2a2a5c4426 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 00:12:54 +0900 Subject: [PATCH 09/87] =?UTF-8?q?#13=20ci:=20application=20yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD(be=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9523ab203..670ee3c47 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,7 @@ on: env: working-directory: ./be - APPLICATION_YML_FILE_PATH: ./src/main/resources/application.yml + APPLICATION_YML_FILE_PATH: /issue/src/main/resources/application.yml AWS_REGION: ap-northeast-2 S3_BUCKET_NAME: issue-tracker-s3-bucket CODE_DEPLOY_APPLICATION_NAME: issue-tracker-codedeploy-app From 08cffdbd0807e3f84e5a3435ddf99e0326cb0a2f Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 00:21:17 +0900 Subject: [PATCH 10/87] =?UTF-8?q?#13=20ci:=20application=20yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD(issue=20?= =?UTF-8?q?=EC=83=81=EC=9C=84=20=EA=B2=BD=EB=A1=9C=20=EC=A7=80=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 670ee3c47..3150929f2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,7 @@ on: env: working-directory: ./be - APPLICATION_YML_FILE_PATH: /issue/src/main/resources/application.yml + APPLICATION_YML_FILE_PATH: ./issue/src/main/resources/application.yml AWS_REGION: ap-northeast-2 S3_BUCKET_NAME: issue-tracker-s3-bucket CODE_DEPLOY_APPLICATION_NAME: issue-tracker-codedeploy-app From 5ff739b5ae18deda4792946190bd2e05d2d4ce26 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 00:27:35 +0900 Subject: [PATCH 11/87] =?UTF-8?q?#13=20ci:=20deploy=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20application.yml=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3150929f2..b638ce7bd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,8 +5,7 @@ on: branches: release env: - working-directory: ./be - APPLICATION_YML_FILE_PATH: ./issue/src/main/resources/application.yml + APPLICATION_YML_FILE_PATH: ./src/main/resources/application.yml AWS_REGION: ap-northeast-2 S3_BUCKET_NAME: issue-tracker-s3-bucket CODE_DEPLOY_APPLICATION_NAME: issue-tracker-codedeploy-app @@ -23,7 +22,7 @@ jobs: defaults: run: shell: bash - working-directory: ./be + working-directory: ./be/issue steps: - name: Checkout From f926395eb3006d9ebab2f1cb40b47a3f84be85bc Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 00:37:54 +0900 Subject: [PATCH 12/87] =?UTF-8?q?#13=20ci:=20deploy=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20build=20=EB=B6=80=EB=B6=84=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b638ce7bd..8c10a6a65 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,7 +39,6 @@ jobs: - name: Build with Gradle run: | - cd be/issue chmod +x gradlew ./gradlew clean build -x test From a84dda80b060df19f86c34a872f69fea0863798f Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 01:03:36 +0900 Subject: [PATCH 13/87] =?UTF-8?q?#13=20ci:=20appspec.yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=EC=84=9C=20deploy-app=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appspec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appspec.yml b/appspec.yml index 9e24388bf..fc2999c4f 100644 --- a/appspec.yml +++ b/appspec.yml @@ -14,6 +14,6 @@ permissions: hooks: AfterInstall: - - location: deploy-app.sh + - location: scripts/deploy-app.sh timeout: 60 runas: ubuntu From d0bd417b817d193a15a5dacaf0ad8c8308912a30 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 01:06:29 +0900 Subject: [PATCH 14/87] =?UTF-8?q?#13=20ci:=20appspec.yml,=20scripts=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appspec.yml | 19 ------------------- {scripts => be/issue/scripts}/deploy-app.sh | 0 2 files changed, 19 deletions(-) delete mode 100644 appspec.yml rename {scripts => be/issue/scripts}/deploy-app.sh (100%) diff --git a/appspec.yml b/appspec.yml deleted file mode 100644 index fc2999c4f..000000000 --- a/appspec.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 0.0 -os: linux - -files: - - source: / - destination: /home/ubuntu/app - overwrite: yes - -permissions: - - object: / - pattern: "**" - owner: ubuntu - group: ubuntu - -hooks: - AfterInstall: - - location: scripts/deploy-app.sh - timeout: 60 - runas: ubuntu diff --git a/scripts/deploy-app.sh b/be/issue/scripts/deploy-app.sh similarity index 100% rename from scripts/deploy-app.sh rename to be/issue/scripts/deploy-app.sh From d647f26a63f141ea927f71b11d78dc184bfe6dc7 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 01:27:53 +0900 Subject: [PATCH 15/87] =?UTF-8?q?#13=20ci:=20appspec.yml,=20scripts=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/issue/{scripts => }/deploy-app.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename be/issue/{scripts => }/deploy-app.sh (100%) diff --git a/be/issue/scripts/deploy-app.sh b/be/issue/deploy-app.sh similarity index 100% rename from be/issue/scripts/deploy-app.sh rename to be/issue/deploy-app.sh From 04f0a9889cb9e097a922dd0bffea6e742ea459c8 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 01:35:50 +0900 Subject: [PATCH 16/87] =?UTF-8?q?#13=20ci:=20appspec.yml=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/issue/.gitignore | 2 +- be/issue/appspec.yml | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 be/issue/appspec.yml diff --git a/be/issue/.gitignore b/be/issue/.gitignore index 649136dfb..c489d2899 100644 --- a/be/issue/.gitignore +++ b/be/issue/.gitignore @@ -36,4 +36,4 @@ out/ ### VS Code ### .vscode/ -*.yml + diff --git a/be/issue/appspec.yml b/be/issue/appspec.yml new file mode 100644 index 000000000..9e24388bf --- /dev/null +++ b/be/issue/appspec.yml @@ -0,0 +1,19 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/app + overwrite: yes + +permissions: + - object: / + pattern: "**" + owner: ubuntu + group: ubuntu + +hooks: + AfterInstall: + - location: deploy-app.sh + timeout: 60 + runas: ubuntu From f1fd82e45a09999ce0269bc9be2a3b64c09f9a80 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Tue, 8 Aug 2023 01:44:54 +0900 Subject: [PATCH 17/87] =?UTF-8?q?#13=20ci:=20deploy-app=20jar=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/issue/deploy-app.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/be/issue/deploy-app.sh b/be/issue/deploy-app.sh index 2a09b14a5..0d14bcc7f 100644 --- a/be/issue/deploy-app.sh +++ b/be/issue/deploy-app.sh @@ -1,12 +1,12 @@ -KIOSK_ID=$(jps | grep be | awk '{ print $1 }') +ISSUE_ID=$(jps | grep be | awk '{ print $1 }') -if [ -z $KIOSK_ID ]; then +if [ -z $ISSUE_ID ]; then echo "동작중인 서버가 없습니다." else - echo "$KIOSK_ID 프로세스를 삭제합니다." - kill -9 $KIOSK_ID + echo "$ISSUE_ID 프로세스를 삭제합니다." + kill -9 $ISSUE_ID fi echo "서버 시작" -nohup java -jar ~/app/issue-0.0.1-SNAPSHOT.jar >/home/ubuntu/app/log.txt 2>&1 & -echo "배포 성공" \ No newline at end of file +nohup java -jar ~/app/build/libs/issue-0.0.1-SNAPSHOT.jar >/home/ubuntu/app/log.txt 2>&1 & +echo "배포 성공" From 11995646238d5a896a46147fb1d87c7de301eafd Mon Sep 17 00:00:00 2001 From: fuse Date: Mon, 7 Aug 2023 15:04:20 +0900 Subject: [PATCH 18/87] =?UTF-8?q?#26=20feat:=20Context=EC=99=80=20useAuth?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 34 ++++++++++------------ frontend/src/context/AuthProvider.tsx | 29 ++++++++++++++++++ frontend/src/hooks/useAuth.ts | 10 +++++++ frontend/src/main.tsx | 14 +++++++-- frontend/src/pages/AddIssue.tsx | 3 ++ frontend/src/pages/Login.tsx | 22 ++++++++++---- frontend/src/pages/Main.tsx | 21 ++++++++++--- frontend/src/routes/AuthenticatedRoute.tsx | 17 ----------- frontend/src/routes/RequireAuth.tsx | 15 ++++++++++ 9 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/pages/AddIssue.tsx delete mode 100644 frontend/src/routes/AuthenticatedRoute.tsx create mode 100644 frontend/src/routes/RequireAuth.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7a898aa46..4c138524d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,18 +3,19 @@ import { ThemeProvider } from 'styled-components'; import { lightTheme } from './theme'; import { darkTheme } from './theme'; import GlobalStyle from './style/Global'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; import Components from './pages/Components'; import Login from './pages/Login'; import Register from './pages/Register'; import Main from './pages/Main'; +import AddIssue from './pages/AddIssue'; import LogoDarkLarge from './asset/logo/logo_dark_large.svg'; import LogoDarkMedium from './asset/logo/logo_dark_medium.svg'; import LogoLightLarge from './asset/logo/logo_light_large.svg'; import LogoLightMedium from './asset/logo/logo_light_medium.svg'; import { AppContext } from './main'; -import AuthenticatedRoute from './routes/AuthenticatedRoute'; +import RequireAuth from './routes/RequireAuth'; function App() { const [isLight, setIsLight] = useState(true); @@ -36,23 +37,18 @@ function App() { return ( - - - - - } /> - - - } - /> - } /> - } /> - } /> - - + + {/* public routes */} + } /> + } /> + } /> + + {/* protected routes */} + }> + } /> + } /> + + ); } diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 000000000..5ff1d629c --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,29 @@ +import { useState, createContext, ReactElement } from 'react'; + +type AuthUser = { user: string; pwd: string; accessToken: string }; + +export type AuthContextValue = { + auth: AuthUser | null; + login: (authUser: AuthUser) => void; + logout: () => void; +}; + +export const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactElement }) { + const [auth, setAuth] = useState(null); + + const login = (authUser: AuthUser) => { + setAuth(authUser); + }; + + const logout = () => { + setAuth(null); + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 000000000..198924ef9 --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { AuthContext, AuthContextValue } from '../context/AuthProvider'; + +export const useAuth = (): AuthContextValue => { + const authContext = useContext(AuthContext); + if (!authContext) { + throw new Error('useAuth must be used within an AuthProvider.'); + } + return authContext; +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index eba4bef86..71a2bd6a8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import { worker } from './mocks/browser'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { AuthProvider } from './context/AuthProvider.tsx'; export const API_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL; @@ -20,8 +22,14 @@ export const AppContext = React.createContext(appContext); ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + } /> + + + + ); diff --git a/frontend/src/pages/AddIssue.tsx b/frontend/src/pages/AddIssue.tsx new file mode 100644 index 000000000..7c0cf6f84 --- /dev/null +++ b/frontend/src/pages/AddIssue.tsx @@ -0,0 +1,3 @@ +export default function AddIssue() { + return

이슈 작성 페이지

; +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index ec3ceed91..96d1b790c 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,17 +1,22 @@ import { useContext } from 'react'; import { styled } from 'styled-components'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { AppContext } from '../main'; +import { useAuth } from '../hooks/useAuth'; import ContextLogo from '../types/ContextLogo'; import TextInput from '../components/common/TextInput'; import ButtonLarge from '../components/common/button/ButtonLarge'; import Button from '../components/common/button/BaseButton'; export default function Login() { - const { util, control } = useContext(AppContext); + const { login } = useAuth(); + const { util } = useContext(AppContext); const logo = (util.getLogoByTheme() as ContextLogo).large; const navigate = useNavigate(); - const login = (id: string, password: string) => { + const location = useLocation(); + const from = location.state?.from?.pathname || '/'; + + const handleLogin = (id: string, password: string) => { (async function () { const res = await fetch('/api/login', { method: 'POST', @@ -25,9 +30,14 @@ export default function Login() { if (res.status === 200) { localStorage.setItem('accessToken', data.messages.accessToken); localStorage.setItem('refreshToken', data.messages.refreshToken); - control.loginCheck(); + login({ + user: id, + pwd: password, + accessToken: data.messages.accessToken, + }); } - navigate('/'); + + navigate(from, { replace: true }); })(); }; @@ -39,7 +49,7 @@ export default function Login() { id: string; password: string; }; - login(id, password); + handleLogin(id, password); }; return ( diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index 1fa7298e5..e2c03273b 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -1,12 +1,25 @@ -import { useContext } from 'react'; -import { AppContext } from '../main'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; export default function Main() { - const { control } = useContext(AppContext); + const auth = useAuth(); + const navigate = useNavigate(); + + const handleLogout = () => { + auth.logout(); + navigate('/login'); + }; + return ( <>

main

- + + ); } diff --git a/frontend/src/routes/AuthenticatedRoute.tsx b/frontend/src/routes/AuthenticatedRoute.tsx deleted file mode 100644 index 184628bbe..000000000 --- a/frontend/src/routes/AuthenticatedRoute.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useState, useContext } from 'react'; -import { Navigate } from 'react-router'; -import { AppContext } from '../main'; - -export default function AuthenticatedRoute({ - children, -}: { - children: React.ReactNode; -}) { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const { util, control } = useContext(AppContext); - util.isLogin = () => isAuthenticated; - control.loginCheck = () => setIsAuthenticated(true); - control.logoutCheck = () => setIsAuthenticated(false); - - return isAuthenticated ? children : ; -} diff --git a/frontend/src/routes/RequireAuth.tsx b/frontend/src/routes/RequireAuth.tsx new file mode 100644 index 000000000..52f201d2a --- /dev/null +++ b/frontend/src/routes/RequireAuth.tsx @@ -0,0 +1,15 @@ +import { useLocation, Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +function RequireAuth() { + const { auth } = useAuth(); + const location = useLocation(); + + return auth?.user ? ( + + ) : ( + + ); +} + +export default RequireAuth; From abc92efe5c45a8281024af31c45fc95487f82a84 Mon Sep 17 00:00:00 2001 From: fuse Date: Mon, 7 Aug 2023 15:58:45 +0900 Subject: [PATCH 19/87] =?UTF-8?q?#26=20chore:=20handleLogin=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EA=B0=84=EB=8B=A8=ED=9E=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Login.tsx | 9 ++++++--- frontend/src/pages/Main.tsx | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 96d1b790c..8b1d23dc8 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -14,10 +14,11 @@ export default function Login() { const logo = (util.getLogoByTheme() as ContextLogo).large; const navigate = useNavigate(); const location = useLocation(); + // 로그인 화면 넘어오기 전 페이지를 기억해서 로그인 성공 시 그 페이지로 이동 const from = location.state?.from?.pathname || '/'; - const handleLogin = (id: string, password: string) => { - (async function () { + const handleLogin = async (id: string, password: string) => { + try { const res = await fetch('/api/login', { method: 'POST', headers: { @@ -38,7 +39,9 @@ export default function Login() { } navigate(from, { replace: true }); - })(); + } catch (err) { + console.error(err); + } }; const handleSubmit = (e: React.FormEvent) => { diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index e2c03273b..4ff820e1d 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -7,6 +7,7 @@ export default function Main() { const handleLogout = () => { auth.logout(); + localStorage.clear(); navigate('/login'); }; From 741d550412071e2d655567adb94e2d7181cc89a4 Mon Sep 17 00:00:00 2001 From: fuse Date: Mon, 7 Aug 2023 16:25:09 +0900 Subject: [PATCH 20/87] =?UTF-8?q?#26=20chore:=20axios=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=20=EB=B0=8F=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 152 +++++++++++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/api/axios.ts | 12 +++ frontend/src/main.tsx | 2 - frontend/src/pages/Login.tsx | 23 +++--- 5 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 frontend/src/api/axios.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5ad8463ca..60792b6d5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", @@ -3178,6 +3179,11 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -3190,6 +3196,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", @@ -3593,6 +3609,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3721,6 +3748,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4246,6 +4281,25 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4255,6 +4309,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -5074,6 +5141,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5616,6 +5702,11 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -8645,12 +8736,27 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "babel-plugin-polyfill-corejs2": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", @@ -8915,6 +9021,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -9010,6 +9124,11 @@ "clone": "^1.0.2" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -9402,6 +9521,11 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -9411,6 +9535,16 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -9996,6 +10130,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -10363,6 +10510,11 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9f1dffe61..c8c047289 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts new file mode 100644 index 000000000..ee798c711 --- /dev/null +++ b/frontend/src/api/axios.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; +const BASE_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL; + +export default axios.create({ + baseURL: BASE_URL, +}); + +export const axiosPrivate = axios.create({ + baseURL: BASE_URL, + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, +}); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 71a2bd6a8..087dad51c 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,8 +5,6 @@ import { worker } from './mocks/browser'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './context/AuthProvider.tsx'; -export const API_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL; - if (import.meta.env.DEV) { worker.start({ onUnhandledRequest: 'bypass', diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 8b1d23dc8..20086eb73 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,5 +1,6 @@ import { useContext } from 'react'; import { styled } from 'styled-components'; +import axios from '../api/axios'; import { useNavigate, useLocation } from 'react-router-dom'; import { AppContext } from '../main'; import { useAuth } from '../hooks/useAuth'; @@ -19,22 +20,22 @@ export default function Login() { const handleLogin = async (id: string, password: string) => { try { - const res = await fetch('/api/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id, password }), - }); - const data = await res.json(); + const res = await axios.post( + '/api/login', + JSON.stringify({ id, password }), + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); if (res.status === 200) { - localStorage.setItem('accessToken', data.messages.accessToken); - localStorage.setItem('refreshToken', data.messages.refreshToken); + localStorage.setItem('accessToken', res.data.messages.accessToken); + localStorage.setItem('refreshToken', res.data.messages.refreshToken); login({ user: id, pwd: password, - accessToken: data.messages.accessToken, + accessToken: res.data.messages.accessToken, }); } From 4b835da166bf62b8cf2c021e09bd31fb1ed26ff1 Mon Sep 17 00:00:00 2001 From: fuse Date: Mon, 7 Aug 2023 17:56:26 +0900 Subject: [PATCH 21/87] =?UTF-8?q?#26=20feat:=20useRefreshToken=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/context/AuthProvider.tsx | 17 +++++++++----- frontend/src/hooks/useAuth.ts | 10 ++++---- frontend/src/hooks/useRefreshToken.ts | 34 +++++++++++++++++++++++++++ frontend/src/main.tsx | 4 +--- frontend/src/mocks/handlers.ts | 2 +- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/Main.tsx | 5 +++- frontend/src/routes/RequireAuth.tsx | 2 +- 8 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 frontend/src/hooks/useRefreshToken.ts diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index 5ff1d629c..726387698 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -1,17 +1,22 @@ import { useState, createContext, ReactElement } from 'react'; -type AuthUser = { user: string; pwd: string; accessToken: string }; +export type AuthUser = { + user: string; + pwd: string; + accessToken: string; +} | null; -export type AuthContextValue = { +export type AuthContextType = { auth: AuthUser | null; + setAuth: React.Dispatch>; login: (authUser: AuthUser) => void; logout: () => void; -}; +} | null; -export const AuthContext = createContext(null); +export const AuthContext = createContext(null); export function AuthProvider({ children }: { children: ReactElement }) { - const [auth, setAuth] = useState(null); + const [auth, setAuth] = useState(null); const login = (authUser: AuthUser) => { setAuth(authUser); @@ -22,7 +27,7 @@ export function AuthProvider({ children }: { children: ReactElement }) { }; return ( - + {children} ); diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 198924ef9..030082dcf 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,10 +1,12 @@ import { useContext } from 'react'; -import { AuthContext, AuthContextValue } from '../context/AuthProvider'; +import { AuthContext } from '../context/AuthProvider'; -export const useAuth = (): AuthContextValue => { +const useAuth = () => { const authContext = useContext(AuthContext); - if (!authContext) { - throw new Error('useAuth must be used within an AuthProvider.'); + if (authContext === null) { + throw new Error('useAuth must be used within an AuthProvider'); } return authContext; }; + +export default useAuth; diff --git a/frontend/src/hooks/useRefreshToken.ts b/frontend/src/hooks/useRefreshToken.ts new file mode 100644 index 000000000..8002ae315 --- /dev/null +++ b/frontend/src/hooks/useRefreshToken.ts @@ -0,0 +1,34 @@ +import axios from '../api/axios'; +import useAuth from './useAuth'; +import { AuthUser } from '../context/AuthProvider'; + +const useRefreshToken = () => { + const { setAuth } = useAuth(); + + const refresh = async () => { + const response = await axios.post( + '/api/reissue/token', + JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') }), + { + withCredentials: true, + } + ); + setAuth((prev) => { + // 이전 상태가 null일 경우 + if (!prev) { + return null; + } + + const newAuth: AuthUser = { + user: prev.user, + pwd: prev.pwd, + accessToken: response.data.accessToken, + }; + return newAuth; + }); + return response.data.accessToken; + }; + return refresh; +}; + +export default useRefreshToken; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 087dad51c..06aae9142 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -23,9 +23,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - } /> - + diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index e67deeb0f..aa32be8d0 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -44,7 +44,7 @@ const successLogin = { const newAccessToken = { accessToken: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkxlbyBLaW0iLCJpYXQiOjE1MTYyMzkwMjJ9.ZfseO7je1qHjBQgT122YZ-OvCMXUQ5NOkVZM8k9P2eU', }; const successGitHubLogin = { diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 20086eb73..84f721470 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -3,7 +3,7 @@ import { styled } from 'styled-components'; import axios from '../api/axios'; import { useNavigate, useLocation } from 'react-router-dom'; import { AppContext } from '../main'; -import { useAuth } from '../hooks/useAuth'; +import useAuth from '../hooks/useAuth'; import ContextLogo from '../types/ContextLogo'; import TextInput from '../components/common/TextInput'; import ButtonLarge from '../components/common/button/ButtonLarge'; diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index 4ff820e1d..b7e07a9e5 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -1,9 +1,11 @@ import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; +import useAuth from '../hooks/useAuth'; +import useRefreshToken from '../hooks/useRefreshToken'; export default function Main() { const auth = useAuth(); const navigate = useNavigate(); + const refresh = useRefreshToken(); const handleLogout = () => { auth.logout(); @@ -21,6 +23,7 @@ export default function Main() { }}> 새로운 이슈작성 + ); } diff --git a/frontend/src/routes/RequireAuth.tsx b/frontend/src/routes/RequireAuth.tsx index 52f201d2a..d24c323fe 100644 --- a/frontend/src/routes/RequireAuth.tsx +++ b/frontend/src/routes/RequireAuth.tsx @@ -1,5 +1,5 @@ import { useLocation, Navigate, Outlet } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; +import useAuth from '../hooks/useAuth'; function RequireAuth() { const { auth } = useAuth(); From 16f3ef60797a39f9627597ee3d87707017052cc0 Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 10:35:00 +0900 Subject: [PATCH 22/87] =?UTF-8?q?#26=20feat:=20useAxiosPrivate=20=ED=9B=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/context/AuthProvider.tsx | 2 +- frontend/src/hooks/useAxiosPrivate.ts | 44 +++++++++++++++++++++++++++ frontend/src/hooks/useRefreshToken.ts | 4 ++- frontend/src/mocks/handlers.ts | 2 +- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/Main.tsx | 31 ++++++++++++++++--- frontend/src/routes/RequireAuth.tsx | 2 +- 7 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 frontend/src/hooks/useAxiosPrivate.ts diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index 726387698..4adb31569 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -1,7 +1,7 @@ import { useState, createContext, ReactElement } from 'react'; export type AuthUser = { - user: string; + userId: string; pwd: string; accessToken: string; } | null; diff --git a/frontend/src/hooks/useAxiosPrivate.ts b/frontend/src/hooks/useAxiosPrivate.ts new file mode 100644 index 000000000..3faa31b9c --- /dev/null +++ b/frontend/src/hooks/useAxiosPrivate.ts @@ -0,0 +1,44 @@ +import { axiosPrivate } from '../api/axios'; +import { useEffect } from 'react'; +import useRefreshToken from './useRefreshToken'; +import useAuth from './useAuth'; + +const useAxiosPrivate = () => { + const refresh = useRefreshToken(); + const { auth } = useAuth(); + + useEffect(() => { + const requestIntercept = axiosPrivate.interceptors.request.use( + (config) => { + if (!config.headers['Authorization']) { + config.headers['Authorization'] = `Bearer ${auth?.accessToken}`; + } + return config; + }, + (error) => Promise.reject(error) + ); + + const responseIntercept = axiosPrivate.interceptors.response.use( + (response) => response, + async (error) => { + const prevRequest = error?.config; + if (error?.response?.status === 403 && !prevRequest?.sent) { + prevRequest.sent = true; + const newAccessToken = await refresh(); + prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; + return axiosPrivate(prevRequest); + } + return Promise.reject(error); + } + ); + + return () => { + axiosPrivate.interceptors.request.eject(requestIntercept); + axiosPrivate.interceptors.response.eject(responseIntercept); + }; + }, [auth, refresh]); + + return axiosPrivate; +}; + +export default useAxiosPrivate; diff --git a/frontend/src/hooks/useRefreshToken.ts b/frontend/src/hooks/useRefreshToken.ts index 8002ae315..17fccc92c 100644 --- a/frontend/src/hooks/useRefreshToken.ts +++ b/frontend/src/hooks/useRefreshToken.ts @@ -20,12 +20,14 @@ const useRefreshToken = () => { } const newAuth: AuthUser = { - user: prev.user, + userId: prev.userId, pwd: prev.pwd, accessToken: response.data.accessToken, }; return newAuth; }); + localStorage.setItem('accessToken', response.data.accessToken); + return response.data.accessToken; }; return refresh; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index aa32be8d0..7b089ec9b 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -36,7 +36,7 @@ const successLogin = { accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', refreshToken: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoIHRva2VuIiwibmFtZSI6IkNvZGVTcXVhZCIsImlhdCI6MTUxNjIzOTAyMn0._VcNtq7pIrC18BZ2OEgkiYosqimrCGzOAwTKPdMR_7g', userId: 1, userProfileImg: 'https://f1.kina.or.kr/2020/11/jtqgmmu4i3.jpg', }, diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 84f721470..c6dfddafb 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -33,7 +33,7 @@ export default function Login() { localStorage.setItem('accessToken', res.data.messages.accessToken); localStorage.setItem('refreshToken', res.data.messages.refreshToken); login({ - user: id, + userId: id, pwd: password, accessToken: res.data.messages.accessToken, }); diff --git a/frontend/src/pages/Main.tsx b/frontend/src/pages/Main.tsx index b7e07a9e5..01680f278 100644 --- a/frontend/src/pages/Main.tsx +++ b/frontend/src/pages/Main.tsx @@ -1,16 +1,37 @@ import { useNavigate } from 'react-router-dom'; +import useAxiosPrivate from '../hooks/useAxiosPrivate'; import useAuth from '../hooks/useAuth'; import useRefreshToken from '../hooks/useRefreshToken'; export default function Main() { - const auth = useAuth(); + const { auth, logout } = useAuth(); const navigate = useNavigate(); const refresh = useRefreshToken(); + const axiosPrivate = useAxiosPrivate(); - const handleLogout = () => { - auth.logout(); - localStorage.clear(); - navigate('/login'); + const handleLogout = async () => { + if (!auth) { + return; + } + + try { + const res = await axiosPrivate.post( + '/api/logout', + JSON.stringify({ id: auth.userId, password: auth.pwd }), + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + + if (res.status === 200) { + localStorage.clear(); + logout(); + } + navigate('/login'); + } catch (err) { + console.error(err); + } }; return ( diff --git a/frontend/src/routes/RequireAuth.tsx b/frontend/src/routes/RequireAuth.tsx index d24c323fe..c35b17dbd 100644 --- a/frontend/src/routes/RequireAuth.tsx +++ b/frontend/src/routes/RequireAuth.tsx @@ -5,7 +5,7 @@ function RequireAuth() { const { auth } = useAuth(); const location = useLocation(); - return auth?.user ? ( + return auth?.userId ? ( ) : ( From 71ed0cdc8d47207aefa56fce8810480af51a55b3 Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 11:56:27 +0900 Subject: [PATCH 23/87] =?UTF-8?q?#26=20feat:=20signup=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Register.tsx | 43 +++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index d2dd1acd1..62d8c75e5 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -1,5 +1,6 @@ import { useContext } from 'react'; import { styled } from 'styled-components'; +import axios from '../api/axios'; import { useNavigate } from 'react-router-dom'; import { AppContext } from '../main'; import ContextLogo from '../types/ContextLogo'; @@ -13,6 +14,37 @@ export default function Register() { .large as string; const navigate = useNavigate(); + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = e.target as HTMLFormElement; + const formData = new FormData(form); + const { userId, password, userName } = Object.fromEntries(formData) as { + userId: string; + password: string; + userName: string; + }; + handleSignUp(userId, password, userName); + }; + + const handleSignUp = async (id: string, password: string, name: string) => { + try { + const res = await axios.post( + '/api/signup', + JSON.stringify({ email: id, password: password, name: name }), + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + + if (res.status === 200) { + navigate('/login', { replace: true }); + } + } catch (err) { + console.error(err); + } + }; + return (

회원가입 페이지

@@ -20,10 +52,10 @@ export default function Register() {
이슈트래커
- + + 회원가입 navigate('/login')}> From 2633916494f86007692545a24fcfc05c0cb8391e Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 14:06:37 +0900 Subject: [PATCH 24/87] =?UTF-8?q?#26=20chore:=20=EA=B0=84=EB=8B=A8?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useAxiosPrivate.ts | 2 +- frontend/src/pages/Login.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useAxiosPrivate.ts b/frontend/src/hooks/useAxiosPrivate.ts index 3faa31b9c..d8534e03d 100644 --- a/frontend/src/hooks/useAxiosPrivate.ts +++ b/frontend/src/hooks/useAxiosPrivate.ts @@ -22,7 +22,7 @@ const useAxiosPrivate = () => { (response) => response, async (error) => { const prevRequest = error?.config; - if (error?.response?.status === 403 && !prevRequest?.sent) { + if (error?.response?.status === 401 && !prevRequest?.sent) { prevRequest.sent = true; const newAccessToken = await refresh(); prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index c6dfddafb..7d123b415 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -17,6 +17,9 @@ export default function Login() { const location = useLocation(); // 로그인 화면 넘어오기 전 페이지를 기억해서 로그인 성공 시 그 페이지로 이동 const from = location.state?.from?.pathname || '/'; + const GITHUB_LOGIN_URL = `https://github.com/login/oauth/authorize?client_id=${ + import.meta.env.VITE_CLIENT_ID + }`; const handleLogin = async (id: string, password: string) => { try { @@ -63,7 +66,14 @@ export default function Login() {
이슈트래커
- + { + window.location.assign( + GITHUB_LOGIN_URL + `&redirect_uri=localhost:5173` + ); + }}> GitHub 계정으로 로그인 or From c141c3ebbdb4fc819c95cbbb12a21df2f9d76952 Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 14:13:53 +0900 Subject: [PATCH 25/87] =?UTF-8?q?#26=20chore:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 06aae9142..1ccb4cbae 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import { worker } from './mocks/browser'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom'; import { AuthProvider } from './context/AuthProvider.tsx'; if (import.meta.env.DEV) { From 3d7571f0b6d45f1c1c28fe3b3dff683dfcb266ee Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 16:01:13 +0900 Subject: [PATCH 26/87] =?UTF-8?q?#26=20feat:=20TextInput=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20type=20prop=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비밀번호를 입력받는 경우에는 type="password"로 설정해야 하므로 TextInput 컴포넌트에 type prop을 추가 --- frontend/src/components/common/TextInput.tsx | 4 +++- frontend/src/pages/Login.tsx | 2 ++ frontend/src/pages/Register.tsx | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/common/TextInput.tsx b/frontend/src/components/common/TextInput.tsx index 3822332fa..b08b3fc4f 100644 --- a/frontend/src/components/common/TextInput.tsx +++ b/frontend/src/components/common/TextInput.tsx @@ -5,6 +5,7 @@ type TextInputProps = React.HTMLAttributes & { size: 'tall' | 'short'; id: string; name: string; + type?: string; labelName: string; disabled?: boolean; placeholder?: string; @@ -21,6 +22,7 @@ export default function TextInput(props: TextInputProps) { id, name, size, + type, labelName, disabled, placeholder, @@ -51,7 +53,7 @@ export default function TextInput(props: TextInputProps) { {(isFocused || inputValue) && } diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index 62d8c75e5..fe36bb4c1 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -64,6 +64,7 @@ export default function Register() { id="password" name="password" size="tall" + type="password" labelName="비밀번호" placeholder="비밀번호" /> From f341875f6dfa440ca19d3ca36916fcb02dfee32b Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 16:02:30 +0900 Subject: [PATCH 27/87] =?UTF-8?q?#26=20feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20userName=EC=9D=84=20=EC=9D=91=EB=8B=B5=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/context/AuthProvider.tsx | 1 + frontend/src/hooks/useRefreshToken.ts | 1 + frontend/src/mocks/handlers.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx index 4adb31569..c9f791b60 100644 --- a/frontend/src/context/AuthProvider.tsx +++ b/frontend/src/context/AuthProvider.tsx @@ -3,6 +3,7 @@ import { useState, createContext, ReactElement } from 'react'; export type AuthUser = { userId: string; pwd: string; + userName: string; accessToken: string; } | null; diff --git a/frontend/src/hooks/useRefreshToken.ts b/frontend/src/hooks/useRefreshToken.ts index 17fccc92c..bfb780507 100644 --- a/frontend/src/hooks/useRefreshToken.ts +++ b/frontend/src/hooks/useRefreshToken.ts @@ -22,6 +22,7 @@ const useRefreshToken = () => { const newAuth: AuthUser = { userId: prev.userId, pwd: prev.pwd, + userName: prev.userName, accessToken: response.data.accessToken, }; return newAuth; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 7b089ec9b..10cd44057 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -38,6 +38,7 @@ const successLogin = { refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZWZyZXNoIHRva2VuIiwibmFtZSI6IkNvZGVTcXVhZCIsImlhdCI6MTUxNjIzOTAyMn0._VcNtq7pIrC18BZ2OEgkiYosqimrCGzOAwTKPdMR_7g', userId: 1, + userName: 'silvertae', userProfileImg: 'https://f1.kina.or.kr/2020/11/jtqgmmu4i3.jpg', }, }; @@ -55,6 +56,7 @@ const successGitHubLogin = { refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', userId: 1, + userName: 'silvertae', userProfileImg: 'https://f1.kina.or.kr/2020/11/jtqgmmu4i3.jpg', }, }; From ba2f52f103795338e9a18c2b8f7d2418abd002a2 Mon Sep 17 00:00:00 2001 From: fuse Date: Tue, 8 Aug 2023 16:47:52 +0900 Subject: [PATCH 28/87] =?UTF-8?q?#26=20fix:=20CORS=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/axios.ts | 4 +++- frontend/src/pages/Login.tsx | 14 +++++++------- frontend/src/pages/Register.tsx | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index ee798c711..e7cad8558 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -1,5 +1,7 @@ import axios from 'axios'; -const BASE_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL; +const BASE_URL = import.meta.env.DEV + ? 'http://ec2-43-202-134-94.ap-northeast-2.compute.amazonaws.com:8080' + : import.meta.env.VITE_API_URL; export default axios.create({ baseURL: BASE_URL, diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 914179f57..ef1ff2cb8 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -21,11 +21,11 @@ export default function Login() { import.meta.env.VITE_CLIENT_ID }`; - const handleLogin = async (id: string, password: string) => { + const handleLogin = async (userId: string, password: string) => { try { const res = await axios.post( '/api/login', - JSON.stringify({ id, password }), + JSON.stringify({ email: userId, password }), { headers: { 'Content-Type': 'application/json' }, withCredentials: true, @@ -36,7 +36,7 @@ export default function Login() { localStorage.setItem('accessToken', res.data.messages.accessToken); localStorage.setItem('refreshToken', res.data.messages.refreshToken); login({ - userId: id, + userId: userId, pwd: password, userName: res.data.messages.userName, accessToken: res.data.messages.accessToken, @@ -53,11 +53,11 @@ export default function Login() { e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); - const { id, password } = Object.fromEntries(formData) as { - id: string; + const { userId, password } = Object.fromEntries(formData) as { + userId: string; password: string; }; - handleLogin(id, password); + handleLogin(userId, password); }; return ( @@ -81,7 +81,7 @@ export default function Login() { Date: Tue, 8 Aug 2023 16:58:39 +0900 Subject: [PATCH 29/87] =?UTF-8?q?#26=20chore:=20axios=20baseURL=20?= =?UTF-8?q?=EC=9B=90=EB=9E=98=EB=8C=80=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/axios.ts | 4 +--- frontend/src/pages/Register.tsx | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index e7cad8558..ee798c711 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -1,7 +1,5 @@ import axios from 'axios'; -const BASE_URL = import.meta.env.DEV - ? 'http://ec2-43-202-134-94.ap-northeast-2.compute.amazonaws.com:8080' - : import.meta.env.VITE_API_URL; +const BASE_URL = import.meta.env.DEV ? '' : import.meta.env.VITE_API_URL; export default axios.create({ baseURL: BASE_URL, diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index 726add078..5fa355ecb 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -34,7 +34,6 @@ export default function Register() { { headers: { 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', }, withCredentials: true, } From d6b98553a63c7f453d260d713d5e613d21c451c9 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Tue, 8 Aug 2023 17:13:35 +0900 Subject: [PATCH 30/87] =?UTF-8?q?#15=20refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=83=80=EC=9E=85=EB=8F=84=20entity=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/codesquad/issueTracker/user/domain/User.java | 2 +- .../java/codesquad/issueTracker/user/service/UserService.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java b/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java index 0aa94bd62..060e8bb5c 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/domain/User.java @@ -30,7 +30,7 @@ public User(Long id, String email, String password, String profileImg, String na } public void validateLoginUser(LoginRequestDto loginRequestDto) { - if (password == null) { + if (password == null || !loginType.equals(LoginType.LOCAL)) { throw new CustomException(ErrorCode.GITHUB_LOGIN_USER); } if (!loginRequestDto.getEmail().equals(email) diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java index 92ebdc67d..5f92c7d02 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java @@ -51,8 +51,6 @@ public LoginResponseDto login(LoginRequestDto loginRequestDto) { .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); user.validateLoginUser(loginRequestDto); - userValidator.validateLoginType(LoginType.LOCAL, user.getLoginType()); - Jwt jwt = jwtProvider.createJwt(Map.of("userId", user.getId())); insertOrUpdateToken(user.getId(), jwt); From 56439aeb2b77e9280779ffba7fe5ac72229196b0 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Tue, 8 Aug 2023 17:14:08 +0900 Subject: [PATCH 31/87] =?UTF-8?q?#15=20refactor=20:=20enum=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B9=84=EA=B5=90=20=EC=8B=9C=20=EB=93=B1=ED=98=B8?= =?UTF-8?q?=20->=20equals=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codesquad/issueTracker/user/service/UserValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java index 7c8927af6..b2c12e4f4 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserValidator.java @@ -15,7 +15,7 @@ public class UserValidator { private final UserRepository userRepository; public void validateLoginType(LoginType inputLoginType, LoginType existLoginType) { - if (inputLoginType != existLoginType) { + if (!inputLoginType.equals(existLoginType)) { throw new CustomException(ErrorCode.FAILED_LOGIN_USER); } } From beff0e7ea3b3c5c212343041c7748f7574c68e65 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Tue, 8 Aug 2023 17:14:16 +0900 Subject: [PATCH 32/87] =?UTF-8?q?#15=20test=20:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserServiceTest.java | 153 ++++++++++++++++++ .../user/service/UserValidatorTest.java | 13 +- 2 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java diff --git a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java new file mode 100644 index 000000000..f49901e2e --- /dev/null +++ b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserServiceTest.java @@ -0,0 +1,153 @@ +package codesquad.issueTracker.user.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mindrot.jbcrypt.BCrypt; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import annotation.ServiceTest; +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.jwt.domain.Jwt; +import codesquad.issueTracker.jwt.domain.Token; +import codesquad.issueTracker.jwt.util.JwtProvider; +import codesquad.issueTracker.user.domain.LoginType; +import codesquad.issueTracker.user.domain.User; +import codesquad.issueTracker.user.dto.LoginRequestDto; +import codesquad.issueTracker.user.dto.LoginResponseDto; +import codesquad.issueTracker.user.repository.UserRepository; + +@ServiceTest +public class UserServiceTest { + + @InjectMocks + UserService userService; + @Mock + UserRepository userRepository; + @Mock + UserValidator userValidator; + @Mock + JwtProvider jwtProvider; + + @DisplayName("로그인 성공") + @Test + void loginSuccess() { + //given + User mockUser = User.builder() + .id(1L) + .email("rkarbf@gmail.com") + .password(BCrypt.hashpw("password", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + + Jwt mockJwt = new Jwt("accessToken", "refreshToken"); + + given(userRepository.findByEmail(mockUser.getEmail())).willReturn(Optional.of(mockUser)); + given(jwtProvider.createJwt(anyMap())).willReturn(mockJwt); + + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "password"); + + //when + LoginResponseDto response = userService.login(loginRequestDto); + + //then + assertAll( + () -> assertEquals(mockUser.getId(), response.getUserId()), + () -> assertEquals("accessToken", response.getAccessToken()), + () -> assertEquals("refreshToken", response.getRefreshToken()) + ); + } + + @DisplayName("존재하지 않는 유저 로그인 실패") + @Test + void loginFailedByNotFoundUser() { + given(userRepository.findByEmail(any())).willReturn(Optional.empty()); + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "password"); + + //when + assertThrows(CustomException.class, () -> userService.login(loginRequestDto)); + } + + @DisplayName("비밀번호 불일치 로그인 실패") + @Test + void loginFailedByWrongPassword() { + User mockUser = User.builder() + .id(1L) + .email("rkarbf@gmail.com") + .password(BCrypt.hashpw("password", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + + given(userRepository.findByEmail(mockUser.getEmail())).willReturn(Optional.of(mockUser)); + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "passwor"); + + //when + assertThrows(CustomException.class, () -> userService.login(loginRequestDto)); + } + + @DisplayName("로그인타입 불일치 로그인 실패") + @Test + void loginFailedByLoginTypeDiff() { + User mockUser = User.builder() + .id(1L) + .email("rkarbf@gmail.com") + .password(BCrypt.hashpw("password", BCrypt.gensalt())) + .loginType(LoginType.GITHUB) + .build(); + + given(userRepository.findByEmail(mockUser.getEmail())).willReturn(Optional.of(mockUser)); + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "password"); + + //when + assertThrows(CustomException.class, () -> userService.login(loginRequestDto)); + } + + @DisplayName("로그인 시 리프레쉬 토큰이 존재하지 않을 때 토큰을 생성하는지 테스트") + @Test + void loginWhenRefreshTokenIsNull() { + User mockUser = User.builder() + .id(1L) + .email("rkarbf@gmail.com") + .password(BCrypt.hashpw("password", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + + Jwt mockJwt = new Jwt("accessToken", "refreshToken"); + + given(userRepository.findByEmail(mockUser.getEmail())).willReturn(Optional.of(mockUser)); + given(jwtProvider.createJwt(anyMap())).willReturn(mockJwt); + given(userRepository.findTokenByUserId(1L)).willReturn(Optional.empty()); + + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "password"); + userService.login(loginRequestDto); + verify(userRepository, times(1)).insertRefreshToken(1L, mockJwt.getRefreshToken()); + verify(userRepository, times(0)).updateRefreshToken(1L, mockJwt.getRefreshToken()); + } + + @DisplayName("로그인 시 리프레쉬 토큰이 존재할 때 토큰을 업데이트하는지 테스트") + @Test + void loginWhenRefreshTokenIsExist() { + User mockUser = User.builder() + .id(1L) + .email("rkarbf@gmail.com") + .password(BCrypt.hashpw("password", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) + .build(); + + Jwt mockJwt = new Jwt("accessToken", "refreshToken"); + + given(userRepository.findByEmail(mockUser.getEmail())).willReturn(Optional.of(mockUser)); + given(jwtProvider.createJwt(anyMap())).willReturn(mockJwt); + given(userRepository.findTokenByUserId(1L)).willReturn(Optional.of(new Token(1L, 1L, "dummy"))); + + LoginRequestDto loginRequestDto = new LoginRequestDto("rkarbf@gmail.com", "password"); + userService.login(loginRequestDto); + verify(userRepository, times(0)).insertRefreshToken(1L, mockJwt.getRefreshToken()); + verify(userRepository, times(1)).updateRefreshToken(1L, mockJwt.getRefreshToken()); + } +} diff --git a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java index 7d3ddcc56..510570c2b 100644 --- a/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java +++ b/be/issue/src/test/java/codesquad/issueTracker/user/service/UserValidatorTest.java @@ -52,10 +52,11 @@ void validateLoginUserFailed() { .id(1L) .email("asdfff123@ddd.com") .password(BCrypt.hashpw("12345678", BCrypt.gensalt())) + .loginType(LoginType.LOCAL) .build(); - given(userRepository.findByEmail("asd123@ddd.com")).willReturn(Optional.of(user)); - User findUser = userRepository.findByEmail("asd123@ddd.com").orElseThrow(); + given(userRepository.findByEmail("asd123@dddd.com")).willReturn(Optional.of(user)); + User findUser = userRepository.findByEmail("asd123@dddd.com").orElseThrow(); LoginRequestDto loginRequestDto = new LoginRequestDto("asd123@dddd.com", "123dd45678"); assertThrows(CustomException.class, () -> { @@ -77,7 +78,7 @@ public void validateLoginTypeSuccess() { given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); LoginType existUserLoginType = userRepository.findByEmail(existUser.getEmail()).get().getLoginType(); assertDoesNotThrow(() -> { - // userValidator.validateLoginType(input, existUserLoginType); + userValidator.validateLoginType(input, existUserLoginType); }); } @@ -96,7 +97,7 @@ public void validateLoginTypeFailed() { given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); LoginType existUserLoginType = userRepository.findByEmail(existUser.getEmail()).get().getLoginType(); assertThrows(CustomException.class, () -> { - // userValidator.validateLoginType(input, existUserLoginType); + userValidator.validateLoginType(input, existUserLoginType); }); } @@ -113,7 +114,7 @@ public void validateDuplicatedEmailSuccess() { given(userRepository.findByEmail(any())).willReturn(Optional.empty()); assertDoesNotThrow(() -> { - // userValidator.validateDuplicatedEmail(signUpRequestDto); + userValidator.validateDuplicatedEmail(signUpRequestDto); }); } @@ -129,7 +130,7 @@ public void validateDuplicatedEmailFailed() { given(userRepository.findByEmail(any())).willReturn(Optional.ofNullable(existUser)); assertThrows(CustomException.class, () -> { - // userValidator.validateDuplicatedEmail(signUpRequestDto); + userValidator.validateDuplicatedEmail(signUpRequestDto); }); } From 81383df8abc322fc98e91b65548d25122638392c Mon Sep 17 00:00:00 2001 From: hyeseon Date: Wed, 9 Aug 2023 01:34:27 +0900 Subject: [PATCH 33/87] =?UTF-8?q?#13=20ci:=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=20cicd=20=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 46 ++++++++++++++++++++++++++++++++++++ frontend/appspec.yml | 20 ++++++++++++++++ frontend/deploy-web.sh | 1 + 3 files changed, 67 insertions(+) create mode 100644 frontend/appspec.yml create mode 100644 frontend/deploy-web.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8c10a6a65..bbd93c0a6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,3 +63,49 @@ jobs: --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $CODE_DEPLOY_DEPLOYMENT_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=Build/$GITHUB_SHA.zip + + web-build-and-deploy: + name: FE Deploy + needs: was-build-and-deploy + runs-on: ubuntu-latest + environment: production + defaults: + run: + shell: bash + working-directory: ./frontend + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Build with npm + run: | + npm install + npm run build + + - name: Make zip file + run: zip -r ./react-build.zip . + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}} + aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}} + aws-region: ${{env.AWS_REGION}} + + - name: Upload to S3 + run: aws s3 cp + --region ${{ env.AWS_REGION }} ./react-build.zip + s3://$S3_BUCKET_NAME/Build/react-build.zip + + - name: Code Deploy + run: aws deploy create-deployment + --application-name $CODE_DEPLOY_APPLICATION_NAME + --deployment-config-name CodeDeployDefault.AllAtOnce + --deployment-group-name $CODE_DEPLOY_DEPLOYMENT_GROUP_NAME + --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=Build/react-build.zip diff --git a/frontend/appspec.yml b/frontend/appspec.yml new file mode 100644 index 000000000..7e77f8687 --- /dev/null +++ b/frontend/appspec.yml @@ -0,0 +1,20 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/dist + overwrite: yes + +permissions: + - object: / + pattern: "**" + owner: ubuntu + group: ubuntu + mode: 755 + +hooks: + AfterInstall: + - location: deploy-web.sh + timeout: 60 + runas: ubuntu diff --git a/frontend/deploy-web.sh b/frontend/deploy-web.sh new file mode 100644 index 000000000..b136adc67 --- /dev/null +++ b/frontend/deploy-web.sh @@ -0,0 +1 @@ +sudo systemctl restart nginx \ No newline at end of file From 26755b594234ab13d9262e3445fca09262e6b5b1 Mon Sep 17 00:00:00 2001 From: hyeseon Date: Wed, 9 Aug 2023 01:38:44 +0900 Subject: [PATCH 34/87] =?UTF-8?q?#13=20ci:=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=20deploy=20=ED=8C=8C=EC=9D=BC=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bbd93c0a6..cb6e41692 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -66,7 +66,7 @@ jobs: web-build-and-deploy: name: FE Deploy - needs: was-build-and-deploy + needs: app-build-and-deploy runs-on: ubuntu-latest environment: production defaults: From bf09c13b85283731bfcf4eb3fd057b4eb5d7626f Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Wed, 9 Aug 2023 14:54:35 +0900 Subject: [PATCH 35/87] =?UTF-8?q?#15=20chore=20:=20=EC=9C=A0=EC=A0=80=20na?= =?UTF-8?q?me=20=EA=B8=B8=EC=9D=B4=20=EC=A0=9C=ED=95=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ++++++++ .../codesquad/issueTracker/user/dto/SignUpRequestDto.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/dto/SignUpRequestDto.java b/be/issue/src/main/java/codesquad/issueTracker/user/dto/SignUpRequestDto.java index b67b36921..cb2029418 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/dto/SignUpRequestDto.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/dto/SignUpRequestDto.java @@ -18,7 +18,7 @@ public class SignUpRequestDto { @Size(min = 6, max = 12, message = "비밀번호는 6자리에서 12자리까지 입력할 수 있습니다.") private final String password; - @Size(min = 6, max = 16, message = "아이디는 최소 6자리에서 16자리까지 입력할 수 있다.") + @Size(min = 2, max = 12, message = "이름은 최소 6자리에서 16자리까지 입력할 수 있다.") private final String name; public static User toEntity(SignUpRequestDto signUpRequestDto, String encodedPassword, String profileImg) { From 31c4fa0b307b91273d10b2e38176541e87664073 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Wed, 9 Aug 2023 14:55:12 +0900 Subject: [PATCH 36/87] =?UTF-8?q?#15=20refactor=20:=20reissue=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=9C=20=EB=B0=98=ED=99=98=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issueTracker/user/controller/UserController.java | 5 +++-- .../codesquad/issueTracker/user/service/UserService.java | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java b/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java index 5a4b6a950..45eefcd26 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/controller/UserController.java @@ -15,6 +15,7 @@ import codesquad.issueTracker.global.ApiResponse; import codesquad.issueTracker.jwt.dto.RequestRefreshTokenDto; +import codesquad.issueTracker.jwt.dto.ResponseAccessToken; import codesquad.issueTracker.oauth.service.OAuthService; import codesquad.issueTracker.user.dto.LoginRequestDto; import codesquad.issueTracker.user.dto.LoginResponseDto; @@ -44,8 +45,8 @@ public ApiResponse login(@Valid @RequestBody LoginRequestDto l } @PostMapping("/reissue/token") - public ApiResponse reissueToken(@RequestBody RequestRefreshTokenDto requestRefreshTokenDto) { - String accessToken = userService.reissueAccessToken(requestRefreshTokenDto); + public ApiResponse reissueToken(@RequestBody RequestRefreshTokenDto requestRefreshTokenDto) { + ResponseAccessToken accessToken = userService.reissueAccessToken(requestRefreshTokenDto); return ApiResponse.success(SUCCESS.getStatus(), accessToken); } diff --git a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java index 5f92c7d02..3282f427e 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java +++ b/be/issue/src/main/java/codesquad/issueTracker/user/service/UserService.java @@ -13,8 +13,8 @@ import codesquad.issueTracker.jwt.domain.Jwt; import codesquad.issueTracker.jwt.domain.Token; import codesquad.issueTracker.jwt.dto.RequestRefreshTokenDto; +import codesquad.issueTracker.jwt.dto.ResponseAccessToken; import codesquad.issueTracker.jwt.util.JwtProvider; -import codesquad.issueTracker.user.domain.LoginType; import codesquad.issueTracker.user.domain.User; import codesquad.issueTracker.user.dto.LoginRequestDto; import codesquad.issueTracker.user.dto.LoginResponseDto; @@ -87,12 +87,13 @@ public User findExistedOrInsertedUser(User user) { * 2. DB에 없는 리프레시 토큰이면 예외처리 */ @Transactional(readOnly = true) - public String reissueAccessToken(RequestRefreshTokenDto refreshTokenDto) { + public ResponseAccessToken reissueAccessToken(RequestRefreshTokenDto refreshTokenDto) { jwtProvider.getClaims(refreshTokenDto.getRefreshToken()); Token token = userRepository.findTokenByUserToken(refreshTokenDto.getRefreshToken()) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_REFRESH_TOKEN)); - return jwtProvider.reissueAccessToken(Map.of("userId", token.getUserId())); + return new ResponseAccessToken(jwtProvider.reissueAccessToken(Map.of("userId", token.getUserId()))); + } public void logout(HttpServletRequest request) { From 8a62cda743dbec2792600aa6bc2a33df8714c693 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Wed, 9 Aug 2023 18:33:05 +0900 Subject: [PATCH 37/87] =?UTF-8?q?ResponseAccessToken.java=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issueTracker/jwt/dto/ResponseAccessToken.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java new file mode 100644 index 000000000..ded0710a8 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java @@ -0,0 +1,8 @@ +package codesquad.issueTracker.jwt.dto; + +import lombok.Getter; + +@Getter +public class ResponseAccessToken { + private String accessToken; +} From 1a55aff97ff2652e1399ae1035411583606aabff Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Wed, 9 Aug 2023 20:48:29 +0900 Subject: [PATCH 38/87] =?UTF-8?q?#44=20feat=20:=20=EB=A7=88=EC=9D=BC?= =?UTF-8?q?=EC=8A=A4=ED=86=A4=20=EB=93=B1=EB=A1=9D,=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/ErrorCode.java | 14 ++++++-- .../exception/GlobalExceptionHandler.java | 8 +++++ .../jwt/dto/ResponseAccessToken.java | 2 ++ .../controller/MilestoneController.java | 32 +++++++++++++++++ .../milestone/domain/Milestone.java | 34 +++++++++++++++++++ .../dto/SaveMilestoneRequestDto.java | 28 +++++++++++++++ .../repository/MilestoneRepository.java | 33 ++++++++++++++++++ .../milestone/service/MilestoneService.java | 20 +++++++++++ 8 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java index 1789da84f..ac0f8411f 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java @@ -1,5 +1,7 @@ package codesquad.issueTracker.global.exception; +import java.time.DateTimeException; + import org.springframework.http.HttpStatus; import io.jsonwebtoken.ExpiredJwtException; @@ -15,9 +17,8 @@ public enum ErrorCode implements StatusCode { NOT_SUPPORTED_PROVIDER(HttpStatus.BAD_REQUEST, "지원하지 않는 로그인 방식입니다."), GITHUB_LOGIN_USER(HttpStatus.BAD_REQUEST, "이미 깃허브로 로그인한 유저입니다"), - // -- [JWT] -- // - NOT_FOUND_REFRESH_TOKEN(HttpStatus.BAD_REQUEST,"해당하는 리프레시 토큰을 찾을 수 없습니다."), + NOT_FOUND_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "해당하는 리프레시 토큰을 찾을 수 없습니다."), MALFORMED_JWT_EXCEPTION(HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰입니다."), EXPIRED_JWT_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), SIGNATURE_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT의 서명이 올바르지 않습니다."), @@ -28,7 +29,11 @@ public enum ErrorCode implements StatusCode { ALREADY_EXIST_USER(HttpStatus.BAD_REQUEST, "이미 존재하는 유저입니다."), NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "해당하는 유저가 없습니다."), FAILED_LOGIN_USER(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다. 아이디, 비밀번호를 다시 입력해주세요. "), - DELETE_FAIL(HttpStatus.SERVICE_UNAVAILABLE, "DB 서버 오류"); + DELETE_FAIL(HttpStatus.SERVICE_UNAVAILABLE, "DB 서버 오류"), + + // 마일스톤 + INVALIDATE_DATE(HttpStatus.BAD_REQUEST, "현재 날짜보다 이전 날짜 입니다."), + NOT_FOUND_DATE(HttpStatus.BAD_REQUEST, "유효하지 않은 날짜 입니다."); private HttpStatus status; private String message; @@ -65,6 +70,9 @@ public static StatusCode from(RuntimeException e) { if (e instanceof UnsupportedJwtException) { return ErrorCode.UNSUPPORTED_JWT_EXCEPTION; } + if (e instanceof DateTimeException) { + return ErrorCode.NOT_FOUND_DATE; + } return ErrorCode.ILLEGAL_ARGUMENT_EXCEPTION; } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java b/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java index 59be27b22..0304129b5 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package codesquad.issueTracker.global.exception; +import java.time.DateTimeException; import java.util.List; import org.springframework.http.ResponseEntity; @@ -47,5 +48,12 @@ public ResponseEntity> handleExpiredJwtException(ExpiredJwtE .body(ApiResponse.fail(statusCode.getStatus(), statusCode.getMessage())); } + @ExceptionHandler(DateTimeException.class) + public ResponseEntity> handleDateTimeException(DateTimeException e) { + StatusCode statusCode = ErrorCode.from(e); + return ResponseEntity.status(statusCode.getStatus()) + .body(ApiResponse.fail(statusCode.getStatus(), statusCode.getMessage())); + } + } diff --git a/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java b/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java index ded0710a8..25199472f 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java +++ b/be/issue/src/main/java/codesquad/issueTracker/jwt/dto/ResponseAccessToken.java @@ -1,8 +1,10 @@ package codesquad.issueTracker.jwt.dto; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class ResponseAccessToken { private String accessToken; } diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java new file mode 100644 index 000000000..897e47c4c --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/controller/MilestoneController.java @@ -0,0 +1,32 @@ +package codesquad.issueTracker.milestone.controller; + +import static codesquad.issueTracker.global.exception.SuccessCode.*; + +import javax.validation.Valid; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import codesquad.issueTracker.global.ApiResponse; +import codesquad.issueTracker.milestone.dto.SaveMilestoneRequestDto; +import codesquad.issueTracker.milestone.service.MilestoneService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api") +public class MilestoneController { + + private final MilestoneService milestoneService; + + @PostMapping("/milestones") + public ApiResponse milestones(@Valid @RequestBody SaveMilestoneRequestDto request) { + Long milestoneId = milestoneService.save(request); + return ApiResponse.success(SUCCESS.getStatus(), milestoneId); + } + +} \ No newline at end of file diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java new file mode 100644 index 000000000..06ef8b26f --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java @@ -0,0 +1,34 @@ +package codesquad.issueTracker.milestone.domain; + +import java.time.LocalDate; + +import codesquad.issueTracker.global.exception.CustomException; +import codesquad.issueTracker.global.exception.ErrorCode; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Milestone { + + private Long id; + private String name; + private String description; + private LocalDate doneDate; + private Boolean isClosed; + + @Builder + public Milestone(Long id, String name, String description, LocalDate doneDate, Boolean isClosed) { + this.id = id; + this.name = name; + this.description = description; + this.doneDate = doneDate; + this.isClosed = isClosed; + } + + public void validateDate() { + LocalDate currentDate = LocalDate.now(); + if (doneDate.isBefore(currentDate)) { + throw new CustomException(ErrorCode.INVALIDATE_DATE); + } + } +} \ No newline at end of file diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java new file mode 100644 index 000000000..83ebfe524 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java @@ -0,0 +1,28 @@ +package codesquad.issueTracker.milestone.dto; + +import java.time.LocalDate; + +import javax.validation.constraints.Pattern; + +import codesquad.issueTracker.milestone.domain.Milestone; +import lombok.Getter; + +@Getter +public class SaveMilestoneRequestDto { + + private String name; + private String description; + @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "yyyy-mm-dd 형식으로 입력해주세요") + private String doneDate; + private Boolean isClosed; + + public static Milestone toEntity(SaveMilestoneRequestDto request) { + return Milestone.builder() + .name(request.getName()) + .description(request.getDescription()) + .doneDate(LocalDate.parse(request.getDoneDate())) + .isClosed(request.getIsClosed()) + .build(); + } + +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java new file mode 100644 index 000000000..d62d94aed --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java @@ -0,0 +1,33 @@ +package codesquad.issueTracker.milestone.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import codesquad.issueTracker.milestone.domain.Milestone; + +@Repository +public class MilestoneRepository { + private final NamedParameterJdbcTemplate jdbcTemplate; + + public MilestoneRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + + public Long insert(Milestone milestone) { + + String sql = "INSERT INTO milestones(name, description,done_date) VALUES (:name,:description,:doneDate)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + SqlParameterSource parameters = new MapSqlParameterSource() + .addValue("name", milestone.getName()) + .addValue("description", milestone.getDescription()) + .addValue("doneDate", milestone.getDoneDate()); + jdbcTemplate.update(sql, parameters, keyHolder); + return keyHolder.getKey().longValue(); + + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java new file mode 100644 index 000000000..5b862895e --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/service/MilestoneService.java @@ -0,0 +1,20 @@ +package codesquad.issueTracker.milestone.service; + +import org.springframework.stereotype.Service; + +import codesquad.issueTracker.milestone.domain.Milestone; +import codesquad.issueTracker.milestone.dto.SaveMilestoneRequestDto; +import codesquad.issueTracker.milestone.repository.MilestoneRepository; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class MilestoneService { + private final MilestoneRepository milestoneRepository; + + public Long save(SaveMilestoneRequestDto request) { + Milestone milestone = SaveMilestoneRequestDto.toEntity(request); + milestone.validateDate(); + return milestoneRepository.insert(milestone); + } +} From b3f370ca677904465f0bda222007e8979ca1b932 Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Wed, 9 Aug 2023 22:07:39 +0900 Subject: [PATCH 39/87] =?UTF-8?q?#44=20fix=20:=20=EB=A7=88=EC=9D=BC?= =?UTF-8?q?=EC=8A=A4=ED=86=A4=20=EB=93=B1=EB=A1=9D=20request=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A7=88=EA=B0=90=20=EA=B8=B0=ED=95=9C=EC=9D=B4=20?= =?UTF-8?q?null=20=EC=9D=BC=EB=95=8C=20=EC=98=88=EC=99=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/codesquad/issueTracker/milestone/domain/Milestone.java | 2 +- .../issueTracker/milestone/dto/SaveMilestoneRequestDto.java | 2 +- .../issueTracker/milestone/repository/MilestoneRepository.java | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java index 06ef8b26f..bc66c179b 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/domain/Milestone.java @@ -27,7 +27,7 @@ public Milestone(Long id, String name, String description, LocalDate doneDate, B public void validateDate() { LocalDate currentDate = LocalDate.now(); - if (doneDate.isBefore(currentDate)) { + if (doneDate!=null &&doneDate.isBefore(currentDate)) { throw new CustomException(ErrorCode.INVALIDATE_DATE); } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java index 83ebfe524..88a553652 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/dto/SaveMilestoneRequestDto.java @@ -20,7 +20,7 @@ public static Milestone toEntity(SaveMilestoneRequestDto request) { return Milestone.builder() .name(request.getName()) .description(request.getDescription()) - .doneDate(LocalDate.parse(request.getDoneDate())) + .doneDate(request.getDoneDate()!=null?LocalDate.parse(request.getDoneDate()):null) .isClosed(request.getIsClosed()) .build(); } diff --git a/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java b/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java index d62d94aed..c47ba2cd4 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java +++ b/be/issue/src/main/java/codesquad/issueTracker/milestone/repository/MilestoneRepository.java @@ -19,7 +19,6 @@ public MilestoneRepository(JdbcTemplate jdbcTemplate) { } public Long insert(Milestone milestone) { - String sql = "INSERT INTO milestones(name, description,done_date) VALUES (:name,:description,:doneDate)"; KeyHolder keyHolder = new GeneratedKeyHolder(); SqlParameterSource parameters = new MapSqlParameterSource() From cb4a44008f9a686e4c8eb0e295b621b6434c0657 Mon Sep 17 00:00:00 2001 From: "tmdqja0705@naver.com" Date: Wed, 9 Aug 2023 23:21:32 +0900 Subject: [PATCH 40/87] #43 feat : Label CRUD --- .../label/controller/LabelController.java | 51 ++++++++++++ .../dto/CreateLabelResponseDto.java | 16 ++++ .../label/controller/dto/LabelRequestDto.java | 13 ++++ .../controller/dto/LabelResponseDto.java | 22 ++++++ .../label/controller/dto/LabelVO.java | 32 ++++++++ .../issueTracker/label/domain/Label.java | 33 ++++++++ .../label/repository/LabelRepository.java | 77 +++++++++++++++++++ .../label/service/LabelService.java | 43 +++++++++++ 8 files changed, 287 insertions(+) create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/CreateLabelResponseDto.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelRequestDto.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelResponseDto.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelVO.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java create mode 100644 be/issue/src/main/java/codesquad/issueTracker/label/service/LabelService.java diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java new file mode 100644 index 000000000..7363fcc31 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/controller/LabelController.java @@ -0,0 +1,51 @@ +package codesquad.issueTracker.label.controller; + +import static codesquad.issueTracker.global.exception.SuccessCode.*; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import codesquad.issueTracker.global.ApiResponse; +import codesquad.issueTracker.label.controller.dto.LabelRequestDto; +import codesquad.issueTracker.label.controller.dto.CreateLabelResponseDto; +import codesquad.issueTracker.label.controller.dto.LabelResponseDto; +import codesquad.issueTracker.label.service.LabelService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api") +public class LabelController { + + private final LabelService labelService; + + @PostMapping("/labels") + public ApiResponse create(@RequestBody LabelRequestDto labelRequestDto) { + CreateLabelResponseDto createLabelResponseDto = labelService.save(labelRequestDto); + return ApiResponse.success(SUCCESS.getStatus(), createLabelResponseDto); + } + + @PatchMapping("labels/{labelId}") + public ApiResponse modify(@PathVariable Long labelId, @RequestBody LabelRequestDto labelRequestDto) { + labelService.modify(labelId, labelRequestDto); + return ApiResponse.success(SUCCESS.getStatus(), SUCCESS.getMessage()); + } + + @DeleteMapping("labels/{labelId}") + public ApiResponse delete(@PathVariable Long labelId) { + labelService.delete(labelId); + return ApiResponse.success(SUCCESS.getStatus(), SUCCESS.getMessage()); + } + + @GetMapping("labels") + public ApiResponse findAll(){ + LabelResponseDto labelResponseDto = labelService.findAll(); + return ApiResponse.success(SUCCESS.getStatus(), labelResponseDto); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/CreateLabelResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/CreateLabelResponseDto.java new file mode 100644 index 000000000..11999bf8b --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/CreateLabelResponseDto.java @@ -0,0 +1,16 @@ +package codesquad.issueTracker.label.controller.dto; + +import lombok.Getter; + +@Getter +public class CreateLabelResponseDto { + private Long labelId; + + public CreateLabelResponseDto(Long labelId) { + this.labelId = labelId; + } + + public static CreateLabelResponseDto from(Long id){ + return new CreateLabelResponseDto(id); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelRequestDto.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelRequestDto.java new file mode 100644 index 000000000..a3b3f08cb --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelRequestDto.java @@ -0,0 +1,13 @@ +package codesquad.issueTracker.label.controller.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class LabelRequestDto { + private String name; + private String textColor; + private String backgroundColor; + private String description; +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelResponseDto.java new file mode 100644 index 000000000..6e0a61997 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelResponseDto.java @@ -0,0 +1,22 @@ +package codesquad.issueTracker.label.controller.dto; + +import java.util.List; + +import codesquad.issueTracker.label.domain.Label; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +public class LabelResponseDto { + private List labels; + private int milestoneCount; + + public LabelResponseDto(List labels, int milestoneCount) { + this.labels = labels; + this.milestoneCount = milestoneCount; + } + + public static LabelResponseDto of(List labels, int milestoneCount){ + return new LabelResponseDto(labels, milestoneCount); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelVO.java b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelVO.java new file mode 100644 index 000000000..dcde1e553 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/controller/dto/LabelVO.java @@ -0,0 +1,32 @@ +package codesquad.issueTracker.label.controller.dto; + +import java.util.Objects; + +import codesquad.issueTracker.label.domain.Label; + +public class LabelVO { + private final Long id; + private final String name; + + @Override + public int hashCode() { + return Objects.hash(id, name, textColor, backgroundColor, description); + } + + private final String textColor; + private final String backgroundColor; + private final String description; + + public LabelVO(Long id, String name, String textColor, String backgroundColor, String description) { + this.id = id; + this.name = name; + this.textColor = textColor; + this.backgroundColor = backgroundColor; + this.description = description; + } + + public static LabelVO from(Label label){ + return new LabelVO(label.getId(), label.getName(), label.getTextColor(), label.getBackgroundColor(), + label.getDescription()); + } +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java b/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java new file mode 100644 index 000000000..f43884fb5 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/domain/Label.java @@ -0,0 +1,33 @@ +package codesquad.issueTracker.label.domain; + +import codesquad.issueTracker.label.controller.dto.LabelRequestDto; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Label { + Long id; + String name; + String textColor; + String backgroundColor; + String description; + + @Builder + public Label(Long id, String name, String textColor, String backgroundColor, String description) { + this.id = id; + this.name = name; + this.textColor = textColor; + this.backgroundColor = backgroundColor; + this.description = description; + } + + public static Label toEntity(LabelRequestDto labelRequestDto) { + return Label.builder() + .name(labelRequestDto.getName()) + .textColor(labelRequestDto.getTextColor()) + .backgroundColor(labelRequestDto.getBackgroundColor()) + .description(labelRequestDto.getDescription()) + .build(); + } + +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java b/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java new file mode 100644 index 000000000..e0c33bfa3 --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/label/repository/LabelRepository.java @@ -0,0 +1,77 @@ +package codesquad.issueTracker.label.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.relational.core.sql.In; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import codesquad.issueTracker.label.domain.Label; + +@Repository +public class LabelRepository { + + private final NamedParameterJdbcTemplate jdbcTemplate; + public LabelRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Long insert(Label label) { + String sql = "INSERT INTO labels (name,text_color,background_color,description) VALUES (:name,:text_color,:background_color,:description)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", label.getName()); + params.addValue("text_color", label.getTextColor()); + params.addValue("background_color", label.getBackgroundColor()); + params.addValue("description", label.getDescription()); + + jdbcTemplate.update(sql, params, keyHolder); + return keyHolder.getKey().longValue(); + } + + public int update(Long id, Label label) { + String sql = "UPDATE labels SET name = :name, text_color = :textColor, background_color = :backgroundColor, description = :description WHERE id = :id"; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", id); + params.addValue("name", label.getName()); + params.addValue("textColor", label.getTextColor()); + params.addValue("backgroundColor", label.getBackgroundColor()); + params.addValue("description", label.getDescription()); + return jdbcTemplate.update(sql, params); + } + + public int delete(Long id) { + String sql = "UPDATE labels SET is_deleted = TRUE where id = :id"; + return jdbcTemplate.update(sql, new MapSqlParameterSource() + .addValue("id", id)); + } + + public Optional> findAll() { + String sql = "SELECT id, name, text_color, background_color, description " + + "FROM labels " + + "WHERE is_deleted = FALSE"; + return Optional.of(jdbcTemplate.query(sql, labelRowMapper)); + } + + public int findMilestonesCount(){ + String sql = "SELECT COUNT(*) FROM milestones WHERE is_deleted = false"; + MapSqlParameterSource params = new MapSqlParameterSource(); + + return jdbcTemplate.queryForObject(sql, params, Integer.class); + } + + private final RowMapper