From a36cffb30977d13c0a70bb8b13b6d71b4a6aa854 Mon Sep 17 00:00:00 2001 From: jangyeeunee Date: Fri, 16 May 2025 10:21:19 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84(no=20?= =?UTF-8?q?email=20=EC=9D=B8=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/controller/AuthController.java | 49 +++++++++++++++++++ .../server/backend/dto/SignUpRequestDTO.java | 23 +++++++++ .../unideal/server/backend/entity/User.java | 31 ++++++++++++ .../backend/repository/UserRepository.java | 8 +++ .../server/backend/service/UserService.java | 35 +++++++++++++ src/main/resources/application.yml | 5 ++ src/main/resources/templates/login.html | 22 +++++++++ src/main/resources/templates/signup.html | 21 ++++++++ 8 files changed, 194 insertions(+) create mode 100644 src/main/java/kr/unideal/server/backend/controller/AuthController.java create mode 100644 src/main/java/kr/unideal/server/backend/dto/SignUpRequestDTO.java create mode 100644 src/main/java/kr/unideal/server/backend/entity/User.java create mode 100644 src/main/java/kr/unideal/server/backend/repository/UserRepository.java create mode 100644 src/main/java/kr/unideal/server/backend/service/UserService.java create mode 100644 src/main/resources/templates/login.html create mode 100644 src/main/resources/templates/signup.html diff --git a/src/main/java/kr/unideal/server/backend/controller/AuthController.java b/src/main/java/kr/unideal/server/backend/controller/AuthController.java new file mode 100644 index 0000000..42fae4b --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/controller/AuthController.java @@ -0,0 +1,49 @@ +package kr.unideal.server.backend.controller; + +import jakarta.validation.Valid; +import kr.unideal.server.backend.dto.SignUpRequestDTO; +import kr.unideal.server.backend.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequiredArgsConstructor +public class AuthController { + + private final UserService userService; + + //signup 페이지 GET + @GetMapping("/signup") + public String signupPage(Model model) { + model.addAttribute("signUpRequestDTO", new SignUpRequestDTO()); + return "signup"; + } + + @GetMapping("/login") + public String loginPage(Model model) { + model.addAttribute("logInRequestDTO", new L) + } + + @PostMapping("/auth/signup") + public String signup(@Valid @ModelAttribute SignUpRequestDTO signUpRequestDTO, + BindingResult bindingResult, + Model model) { + + if (bindingResult.hasErrors()) { + model.addAttribute("error", "입력값을 다시 확인해주세요."); + return "signup"; + } + + try { + userService.register(signUpRequestDTO); + } catch (IllegalArgumentException e) { + model.addAttribute("error", e.getMessage()); + return "signup"; + } + + return "redirect:/login"; + } +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/dto/SignUpRequestDTO.java b/src/main/java/kr/unideal/server/backend/dto/SignUpRequestDTO.java new file mode 100644 index 0000000..53b21d1 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/SignUpRequestDTO.java @@ -0,0 +1,23 @@ +package kr.unideal.server.backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class SignUpRequestDTO { + @Getter + @Setter + + @NotBlank + @Email + private String email; + + @NotBlank + private String password; + + @NotBlank + private String name; +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/entity/User.java b/src/main/java/kr/unideal/server/backend/entity/User.java new file mode 100644 index 0000000..336968c --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/entity/User.java @@ -0,0 +1,31 @@ +package kr.unideal.server.backend.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "`user`") // user는 예약어이므로 백틱 사용 +@Getter +@Setter +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false, unique = true, length = 100) + private String email; + + @Column(nullable = false, length = 255) + private String password; + + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false) + private boolean isVerified = false; + + @Column + private String verificationToken; +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/repository/UserRepository.java b/src/main/java/kr/unideal/server/backend/repository/UserRepository.java new file mode 100644 index 0000000..b8fbc78 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/repository/UserRepository.java @@ -0,0 +1,8 @@ +package kr.unideal.server.backend.repository; + +import kr.unideal.server.backend.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/service/UserService.java b/src/main/java/kr/unideal/server/backend/service/UserService.java new file mode 100644 index 0000000..4511667 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/service/UserService.java @@ -0,0 +1,35 @@ +package kr.unideal.server.backend.service; + + +import kr.unideal.server.backend.dto.SignUpRequestDTO; +import kr.unideal.server.backend.entity.User; +import kr.unideal.server.backend.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + public void register(SignUpRequestDTO dto) { + if (userRepository.existsByEmail(dto.getEmail())) { + throw new IllegalArgumentException("이미 등록된 이메일입니다."); + } + + System.out.println("이메일: " + dto.getEmail()); + System.out.println("비밀번호: " + dto.getPassword()); + System.out.println("이름: " + dto.getName()); + + User user = new User(); + user.setEmail(dto.getEmail()); + user.setPassword(dto.getPassword()); + user.setName(dto.getName()); + + //이메일 인증이 완료 되어야 회원가입을 진행할 예정이라 일단 verified = TRUE 로 해둠 + user.setVerified(true); +// user.setVerificationToken(UUID.randomUUID().toString()); + + userRepository.save(user); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c13b271..3970d16 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,6 +6,11 @@ spring: ddl-auto: update # Verbose logging for SQL show-sql: true + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/UniDeal + username: root + password: 'tndk1008' springdoc: api-docs: diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..37e4c70 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,22 @@ + + + + + 로그인 + + +

로그인

+ +
+
+ + +
+ + +
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..c22a0d8 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,21 @@ + + + + 회원가입 + + +

회원가입

+
+ +
+ + +
+ + +
+ + +
+ + \ No newline at end of file From 9b38e9555adef683810386d9a4690a201b3954cd Mon Sep 17 00:00:00 2001 From: jangyeeunee Date: Fri, 16 May 2025 10:33:23 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84/=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EB=B0=9C=EA=B8=89=20=EC=97=86=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/controller/AuthController.java | 28 ++++++++++++++++++- .../server/backend/dto/LogInRequestDTO.java | 17 +++++++++++ .../server/backend/dto/LogInResponseDTO.java | 4 +++ .../backend/repository/UserRepository.java | 6 ++++ .../server/backend/service/UserService.java | 20 +++++++++++-- 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/main/java/kr/unideal/server/backend/dto/LogInRequestDTO.java create mode 100644 src/main/java/kr/unideal/server/backend/dto/LogInResponseDTO.java diff --git a/src/main/java/kr/unideal/server/backend/controller/AuthController.java b/src/main/java/kr/unideal/server/backend/controller/AuthController.java index 42fae4b..31bb256 100644 --- a/src/main/java/kr/unideal/server/backend/controller/AuthController.java +++ b/src/main/java/kr/unideal/server/backend/controller/AuthController.java @@ -1,7 +1,10 @@ package kr.unideal.server.backend.controller; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; +import kr.unideal.server.backend.dto.LogInRequestDTO; import kr.unideal.server.backend.dto.SignUpRequestDTO; +import kr.unideal.server.backend.entity.User; import kr.unideal.server.backend.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; @@ -22,9 +25,12 @@ public String signupPage(Model model) { return "signup"; } + + //login 페이지 Get @GetMapping("/login") public String loginPage(Model model) { - model.addAttribute("logInRequestDTO", new L) + model.addAttribute("logInRequestDTO", new LogInRequestDTO()); + return "login"; } @PostMapping("/auth/signup") @@ -46,4 +52,24 @@ public String signup(@Valid @ModelAttribute SignUpRequestDTO signUpRequestDTO, return "redirect:/login"; } + + @PostMapping("/auth/login") + public String login(@ModelAttribute @Valid LogInRequestDTO loginRequestDTO, + BindingResult bindingResult, + Model model, + HttpSession session) { + if (bindingResult.hasErrors()) { + model.addAttribute("error", "입력값을 다시 확인해주세요."); + return "login"; + } + + try { + User user = userService.login(loginRequestDTO); + session.setAttribute("user", user); // 로그인 상태 저장 + return "redirect:/"; // 홈 또는 마이페이지 등으로 이동 + } catch (IllegalArgumentException e) { + model.addAttribute("error", e.getMessage()); + return "login"; + } + } } \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/dto/LogInRequestDTO.java b/src/main/java/kr/unideal/server/backend/dto/LogInRequestDTO.java new file mode 100644 index 0000000..315f57c --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/LogInRequestDTO.java @@ -0,0 +1,17 @@ +package kr.unideal.server.backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LogInRequestDTO { + @NotBlank + @Email + private String email; + + @NotBlank + private String password; +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/dto/LogInResponseDTO.java b/src/main/java/kr/unideal/server/backend/dto/LogInResponseDTO.java new file mode 100644 index 0000000..d721e1c --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/LogInResponseDTO.java @@ -0,0 +1,4 @@ +package kr.unideal.server.backend.dto; + +public class LogInResponseDTO { +} diff --git a/src/main/java/kr/unideal/server/backend/repository/UserRepository.java b/src/main/java/kr/unideal/server/backend/repository/UserRepository.java index b8fbc78..d8ce301 100644 --- a/src/main/java/kr/unideal/server/backend/repository/UserRepository.java +++ b/src/main/java/kr/unideal/server/backend/repository/UserRepository.java @@ -3,6 +3,12 @@ import kr.unideal.server.backend.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + //존재하는 이메일인지 확인 boolean existsByEmail(String email); + + //user의 데이터를 email로 찾음 + Optional findByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/service/UserService.java b/src/main/java/kr/unideal/server/backend/service/UserService.java index 4511667..ccab811 100644 --- a/src/main/java/kr/unideal/server/backend/service/UserService.java +++ b/src/main/java/kr/unideal/server/backend/service/UserService.java @@ -1,6 +1,7 @@ package kr.unideal.server.backend.service; +import kr.unideal.server.backend.dto.LogInRequestDTO; import kr.unideal.server.backend.dto.SignUpRequestDTO; import kr.unideal.server.backend.entity.User; import kr.unideal.server.backend.repository.UserRepository; @@ -11,7 +12,10 @@ @Service @RequiredArgsConstructor public class UserService { + private final UserRepository userRepository; + + //회원가입 db 등록 method public void register(SignUpRequestDTO dto) { if (userRepository.existsByEmail(dto.getEmail())) { throw new IllegalArgumentException("이미 등록된 이메일입니다."); @@ -28,8 +32,20 @@ public void register(SignUpRequestDTO dto) { //이메일 인증이 완료 되어야 회원가입을 진행할 예정이라 일단 verified = TRUE 로 해둠 user.setVerified(true); -// user.setVerificationToken(UUID.randomUUID().toString()); + //user.setVerificationToken(UUID.randomUUID().toString()); userRepository.save(user); - } + }; + + //로그인 정보 확인 method + public User login(LogInRequestDTO dto) { + User user = userRepository.findByEmail(dto.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이메일입니다.")); + + if (!dto.getPassword().matches(user.getPassword())) { + throw new IllegalArgumentException("비밀번호가 올바르지 않습니다."); + } + + return user; + }; } From 3084e60bd5f6f4335b29321f40e4f4315a583e3f Mon Sep 17 00:00:00 2001 From: Alex4386 Date: Fri, 23 May 2025 03:04:43 +0900 Subject: [PATCH 3/6] feat: implement email authentication flow --- .github/workflows/build-staging.yml | 5 ++ .github/workflows/build.yml | 5 ++ build.gradle | 2 + .../backend/controller/AuthController.java | 81 +++++++++---------- .../controller/AuthPageController.java | 33 ++++++++ .../server/backend/dto/SignUpResponseDTO.java | 5 ++ .../server/backend/dto/VerifyRequestDTO.java | 20 +++++ .../server/backend/dto/VerifyResponseDTO.java | 4 + .../unideal/server/backend/entity/User.java | 4 + .../server/backend/service/MailService.java | 35 ++++++++ .../server/backend/service/UserService.java | 68 ++++++++++++++-- .../backend/service/ValidatorService.java | 21 +++++ .../backend/utils/VerificationCodeUtils.java | 23 ++++++ src/main/resources/application.docker.yml | 25 ++++++ src/main/resources/application.yml | 18 +++++ 15 files changed, 300 insertions(+), 49 deletions(-) create mode 100644 src/main/java/kr/unideal/server/backend/controller/AuthPageController.java create mode 100644 src/main/java/kr/unideal/server/backend/dto/SignUpResponseDTO.java create mode 100644 src/main/java/kr/unideal/server/backend/dto/VerifyRequestDTO.java create mode 100644 src/main/java/kr/unideal/server/backend/dto/VerifyResponseDTO.java create mode 100644 src/main/java/kr/unideal/server/backend/service/MailService.java create mode 100644 src/main/java/kr/unideal/server/backend/service/ValidatorService.java create mode 100644 src/main/java/kr/unideal/server/backend/utils/VerificationCodeUtils.java create mode 100644 src/main/resources/application.docker.yml diff --git a/.github/workflows/build-staging.yml b/.github/workflows/build-staging.yml index e9bdade..c9f82d2 100644 --- a/.github/workflows/build-staging.yml +++ b/.github/workflows/build-staging.yml @@ -26,6 +26,11 @@ jobs: - name: 'Set up Docker Buildx' uses: docker/setup-buildx-action@v3 + - name: 'Replace application.yml for docker deployment' + run: | + rm ./src/main/resources/application.yml + mv ./src/main/resources/application.docker.yml ./src/main/resources/application.yml + - name: 'Run gradlew build' run: ./gradlew build diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56f8c39..1f2961f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,11 @@ jobs: - name: 'Set up Docker Buildx' uses: docker/setup-buildx-action@v3 + - name: 'Replace application.yml for docker deployment' + run: | + rm ./src/main/resources/application.yml + mv ./src/main/resources/application.docker.yml ./src/main/resources/application.yml + - name: 'Run gradlew build' run: ./gradlew build diff --git a/build.gradle b/build.gradle index 9f327e2..9229325 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.security:spring-security-core' testImplementation 'org.springframework.boot:spring-boot-starter-test' runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/kr/unideal/server/backend/controller/AuthController.java b/src/main/java/kr/unideal/server/backend/controller/AuthController.java index 31bb256..72e5932 100644 --- a/src/main/java/kr/unideal/server/backend/controller/AuthController.java +++ b/src/main/java/kr/unideal/server/backend/controller/AuthController.java @@ -1,75 +1,74 @@ package kr.unideal.server.backend.controller; -import jakarta.servlet.http.HttpSession; -import jakarta.validation.Valid; -import kr.unideal.server.backend.dto.LogInRequestDTO; -import kr.unideal.server.backend.dto.SignUpRequestDTO; -import kr.unideal.server.backend.entity.User; +import kr.unideal.server.backend.dto.*; import kr.unideal.server.backend.service.UserService; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; +import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -@Controller +@RestController @RequiredArgsConstructor public class AuthController { private final UserService userService; - //signup 페이지 GET - @GetMapping("/signup") - public String signupPage(Model model) { - model.addAttribute("signUpRequestDTO", new SignUpRequestDTO()); - return "signup"; - } + @PostMapping("/auth/signup") + public ResponseEntity signup( + @RequestBody SignUpRequestDTO signUpRequestDTO, + BindingResult bindingResult + ) { + if (bindingResult.hasErrors()) { + throw new IllegalArgumentException("입력값 규격이 올바르지 않습니다."); + } - //login 페이지 Get - @GetMapping("/login") - public String loginPage(Model model) { - model.addAttribute("logInRequestDTO", new LogInRequestDTO()); - return "login"; - } + try { + userService.register(signUpRequestDTO); + } catch (IllegalArgumentException e) { + throw e; + } - @PostMapping("/auth/signup") - public String signup(@Valid @ModelAttribute SignUpRequestDTO signUpRequestDTO, - BindingResult bindingResult, - Model model) { + return ResponseEntity.ok( + new SignUpResponseDTO() + ); + } + // 인증번호 기반 코드 인증 시도 + @PostMapping("/auth/validate") + public ResponseEntity validate(@RequestBody VerifyRequestDTO verifyRequestDTO, BindingResult bindingResult) { if (bindingResult.hasErrors()) { - model.addAttribute("error", "입력값을 다시 확인해주세요."); - return "signup"; + throw new IllegalArgumentException("입력값 규격이 올바르지 않습니다."); } try { - userService.register(signUpRequestDTO); + userService.verifyUser(verifyRequestDTO); } catch (IllegalArgumentException e) { - model.addAttribute("error", e.getMessage()); - return "signup"; + throw e; } - return "redirect:/login"; + return ResponseEntity.ok( + new VerifyResponseDTO() + ); } @PostMapping("/auth/login") - public String login(@ModelAttribute @Valid LogInRequestDTO loginRequestDTO, - BindingResult bindingResult, - Model model, - HttpSession session) { + public ResponseEntity login( + @RequestBody LogInRequestDTO logInRequestDTO, + BindingResult bindingResult + ) { if (bindingResult.hasErrors()) { - model.addAttribute("error", "입력값을 다시 확인해주세요."); - return "login"; + throw new IllegalArgumentException("입력값 규격이 올바르지 않습니다."); } try { - User user = userService.login(loginRequestDTO); - session.setAttribute("user", user); // 로그인 상태 저장 - return "redirect:/"; // 홈 또는 마이페이지 등으로 이동 + userService.login(logInRequestDTO); } catch (IllegalArgumentException e) { - model.addAttribute("error", e.getMessage()); - return "login"; + throw e; } + + return ResponseEntity.ok( + new LogInResponseDTO() + ); } } \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/controller/AuthPageController.java b/src/main/java/kr/unideal/server/backend/controller/AuthPageController.java new file mode 100644 index 0000000..da6729b --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/controller/AuthPageController.java @@ -0,0 +1,33 @@ +package kr.unideal.server.backend.controller; + +import kr.unideal.server.backend.dto.LogInRequestDTO; +import kr.unideal.server.backend.dto.SignUpRequestDTO; +import kr.unideal.server.backend.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +// 대체 왜 여기에 SSR 을???? +// 일단 API는 RestController 사용하는 것이 편하므로 분리 +// +// 아니 근데 FE React 로 만들기로 한거 아녔나.... Thymeleaf 로 왜...? +@Controller +@RequiredArgsConstructor +public class AuthPageController { + + //signup 페이지 GET + @GetMapping("/signup") + public String signupPage(Model model) { + model.addAttribute("signUpRequestDTO", new SignUpRequestDTO()); + return "signup"; + } + + //login 페이지 Get + @GetMapping("/login") + public String loginPage(Model model) { + model.addAttribute("logInRequestDTO", new LogInRequestDTO()); + return "login"; + } + +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/dto/SignUpResponseDTO.java b/src/main/java/kr/unideal/server/backend/dto/SignUpResponseDTO.java new file mode 100644 index 0000000..489dda3 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/SignUpResponseDTO.java @@ -0,0 +1,5 @@ +package kr.unideal.server.backend.dto; + +public class SignUpResponseDTO { + +} diff --git a/src/main/java/kr/unideal/server/backend/dto/VerifyRequestDTO.java b/src/main/java/kr/unideal/server/backend/dto/VerifyRequestDTO.java new file mode 100644 index 0000000..0dc9681 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/VerifyRequestDTO.java @@ -0,0 +1,20 @@ +package kr.unideal.server.backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +public class VerifyRequestDTO { + @Getter + @Setter + + @NotBlank + @Email + String email; + + @Getter + @Setter + @NotBlank + String code; +} diff --git a/src/main/java/kr/unideal/server/backend/dto/VerifyResponseDTO.java b/src/main/java/kr/unideal/server/backend/dto/VerifyResponseDTO.java new file mode 100644 index 0000000..1362018 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/dto/VerifyResponseDTO.java @@ -0,0 +1,4 @@ +package kr.unideal.server.backend.dto; + +public class VerifyResponseDTO { +} diff --git a/src/main/java/kr/unideal/server/backend/entity/User.java b/src/main/java/kr/unideal/server/backend/entity/User.java index 336968c..baf0424 100644 --- a/src/main/java/kr/unideal/server/backend/entity/User.java +++ b/src/main/java/kr/unideal/server/backend/entity/User.java @@ -1,8 +1,12 @@ package kr.unideal.server.backend.entity; import jakarta.persistence.*; +import kr.unideal.server.backend.utils.VerificationCodeUtils; import lombok.Getter; import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Instant; @Entity @Table(name = "`user`") // user는 예약어이므로 백틱 사용 diff --git a/src/main/java/kr/unideal/server/backend/service/MailService.java b/src/main/java/kr/unideal/server/backend/service/MailService.java new file mode 100644 index 0000000..85f2b86 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/service/MailService.java @@ -0,0 +1,35 @@ +package kr.unideal.server.backend.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class MailService { + + @Value("${unideal.mailer.from}") + private String from; + + @Autowired + private JavaMailSender mailSender; + + public void sendSimpleMail(String to, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + + mailSender.send(message); + } + + public void sendVerificationCode(String to, String code) { + this.sendSimpleMail( + to, + "Unideal 가입 인증코드", + "Unideal에 가입해 주셔서 감사합니다. 회원가입을 위한 인증코드는 "+code+" 입니다." + ); + } +} \ No newline at end of file diff --git a/src/main/java/kr/unideal/server/backend/service/UserService.java b/src/main/java/kr/unideal/server/backend/service/UserService.java index ccab811..ff3696c 100644 --- a/src/main/java/kr/unideal/server/backend/service/UserService.java +++ b/src/main/java/kr/unideal/server/backend/service/UserService.java @@ -3,17 +3,27 @@ import kr.unideal.server.backend.dto.LogInRequestDTO; import kr.unideal.server.backend.dto.SignUpRequestDTO; +import kr.unideal.server.backend.dto.VerifyRequestDTO; import kr.unideal.server.backend.entity.User; import kr.unideal.server.backend.repository.UserRepository; +import kr.unideal.server.backend.utils.VerificationCodeUtils; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.time.Instant; + @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final MailService mailService; + private final ValidatorService validatorService; + + private PasswordEncoder passwordEncoder; //회원가입 db 등록 method public void register(SignUpRequestDTO dto) { @@ -21,18 +31,20 @@ public void register(SignUpRequestDTO dto) { throw new IllegalArgumentException("이미 등록된 이메일입니다."); } - System.out.println("이메일: " + dto.getEmail()); - System.out.println("비밀번호: " + dto.getPassword()); - System.out.println("이름: " + dto.getName()); + if (!validatorService.isGachonUnivStudent(dto.getEmail())) { + throw new IllegalArgumentException("가천대학교 학생 이메일이 아닙니다."); + } User user = new User(); user.setEmail(dto.getEmail()); - user.setPassword(dto.getPassword()); user.setName(dto.getName()); + // HASH FIRST - PlainText password in database is security nightmare + user.setPassword(passwordEncoder.encode(dto.getPassword())); + //이메일 인증이 완료 되어야 회원가입을 진행할 예정이라 일단 verified = TRUE 로 해둠 - user.setVerified(true); - //user.setVerificationToken(UUID.randomUUID().toString()); + this.issueVerificationCode(user); + user.setVerified(false); userRepository.save(user); }; @@ -42,10 +54,50 @@ public User login(LogInRequestDTO dto) { User user = userRepository.findByEmail(dto.getEmail()) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이메일입니다.")); - if (!dto.getPassword().matches(user.getPassword())) { - throw new IllegalArgumentException("비밀번호가 올바르지 않습니다."); + if (!user.isVerified()) { + throw new IllegalArgumentException("아직 이메일이 인증되지 않았습니다."); + } + + // bcrypt implementation + String plainText = dto.getPassword(); + String bcrypt = user.getPassword(); + + if (!passwordEncoder.matches(plainText, bcrypt)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); } return user; }; + + // 인증 + public boolean verifyUser(VerifyRequestDTO dto) { + User user = userRepository.findByEmail(dto.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("이메일이 올바르지 않습니다.")); + + if (user.isVerified()) throw new IllegalArgumentException("이미 인증된 사용자입니다."); + + boolean result = this.verifyUser(user, dto.getCode()); + if (result) { + user.setVerified(true); + } + + return result; + } + + private boolean verifyUser(User user, String verificationCode) { + if (user.isVerified()) return true; + + String verificationToken = user.getVerificationToken(); + if (verificationToken == null) { + return false; + } else return verificationToken.equals(verificationCode); + } + + // 인증토큰 발급 + public void issueVerificationCode(User user) { + String verificationCode = VerificationCodeUtils.generateVerificationCode(); + + user.setVerificationToken(verificationCode); + mailService.sendVerificationCode(user.getEmail(), verificationCode); + } } diff --git a/src/main/java/kr/unideal/server/backend/service/ValidatorService.java b/src/main/java/kr/unideal/server/backend/service/ValidatorService.java new file mode 100644 index 0000000..c8925e4 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/service/ValidatorService.java @@ -0,0 +1,21 @@ +package kr.unideal.server.backend.service; + +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; + +@Service +public class ValidatorService { + + public boolean isGachonUnivStudent(String email) { + String[] splitted = email.toLowerCase().split("@"); + String hostname = splitted[splitted.length - 1]; + + if (hostname.endsWith("gachon.ac.kr")) { + // Gachon University hostname + return true; + } + + return false; + } +} diff --git a/src/main/java/kr/unideal/server/backend/utils/VerificationCodeUtils.java b/src/main/java/kr/unideal/server/backend/utils/VerificationCodeUtils.java new file mode 100644 index 0000000..7e393d5 --- /dev/null +++ b/src/main/java/kr/unideal/server/backend/utils/VerificationCodeUtils.java @@ -0,0 +1,23 @@ +package kr.unideal.server.backend.utils; + +import java.security.SecureRandom; + +public class VerificationCodeUtils { + static String verificationCodeCharacters = "0123456789"; + static int verificationCodeLength = 6; + + public static String generateVerificationCode() { + // use system-wide entropy to make sure that + // verification code can not be predicted via + // PRNG seed prediction + SecureRandom random = new SecureRandom(); + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < verificationCodeLength; i++) { + int idx = random.nextInt(verificationCodeCharacters.length()); + builder.append(verificationCodeCharacters.charAt(idx)); + } + + return builder.toString(); + } +} diff --git a/src/main/resources/application.docker.yml b/src/main/resources/application.docker.yml new file mode 100644 index 0000000..be7898d --- /dev/null +++ b/src/main/resources/application.docker.yml @@ -0,0 +1,25 @@ +spring: + application: + name: unideal + jpa: + hibernate: + ddl-auto: update + # Verbose logging for SQL + show-sql: true + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + # NOTE: Do NOT set the db configuration here, + # This would be set by the docker-compose via environment variable. + # + # For more information, Check the following link: + # https://docs.spring.io/spring-boot/docs/3.0.4/reference/html/features.html#features.external-config + # If you configure in this file, the environment variables are automatically overridden, + # causing invalid configuration. If you need to configure some of the parameters, reach me @ Kakaotalk. + +springdoc: + api-docs: + path: /openapi + +unideal: + mailer: + from: test-mail@gmail.com \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3970d16..e673536 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,10 +8,28 @@ spring: show-sql: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver + # == this is only for local deployments == + # For more information, check ./application.docker.yml url: jdbc:mysql://localhost:3306/UniDeal username: root password: 'tndk1008' + mail: + host: smtp.google.com + port: 587 + username: username@example.com + password: password12 + properties: + mail: + smtp: + auth: true + starttls: + enable: true springdoc: api-docs: path: /openapi + +unideal: + mailer: + from: test-mail@gmail.com + From bb81a30c179f5fe9682f662f335ea39e5c707c10 Mon Sep 17 00:00:00 2001 From: Alex4386 Date: Fri, 23 May 2025 03:16:15 +0900 Subject: [PATCH 4/6] chore: remove docker default config --- src/main/resources/application.docker.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/resources/application.docker.yml b/src/main/resources/application.docker.yml index be7898d..0f91af1 100644 --- a/src/main/resources/application.docker.yml +++ b/src/main/resources/application.docker.yml @@ -19,7 +19,3 @@ spring: springdoc: api-docs: path: /openapi - -unideal: - mailer: - from: test-mail@gmail.com \ No newline at end of file From 1957849fcb4972013a35e9c93ac4d5920b34f12d Mon Sep 17 00:00:00 2001 From: Alex4386 Date: Mon, 26 May 2025 12:52:41 +0900 Subject: [PATCH 5/6] chore: revert to mySQL, and add build-all.yml for building for each commit for ALL branches --- .github/workflows/build-all.yml | 50 +++++++++++++++++++++++++++++++++ docker-compose.monolithic.yml | 4 +-- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-all.yml diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml new file mode 100644 index 0000000..432f22d --- /dev/null +++ b/.github/workflows/build-all.yml @@ -0,0 +1,50 @@ +name: Create and publish a Docker image for Each branches + +on: + push: +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + environment: + name: deploy + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: "Checkout repository" + uses: actions/checkout@v4 + + - name: 'Set up Docker Buildx' + uses: docker/setup-buildx-action@v3 + + - name: 'Run gradlew build' + run: ./gradlew build + + - name: "Log in to the Container registry" + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "Extract metadata (tags, labels) for Docker" + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: "Build and push Docker image" + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + file: Dockerfile diff --git a/docker-compose.monolithic.yml b/docker-compose.monolithic.yml index 9fb81ba..0ba2e7d 100644 --- a/docker-compose.monolithic.yml +++ b/docker-compose.monolithic.yml @@ -1,13 +1,13 @@ services: db: - image: mariadb:latest + image: mysql:latest container_name: 'unideal-db' environment: # MYSQL_ROOT_PASSWORD: changeme MYSQL_DATABASE: unideal MYSQL_USER: unideal MYSQL_PASSWORD: changeme - MARIADB_RANDOM_ROOT_PASSWORD: 1 + MYSQL_RANDOM_ROOT_PASSWORD: 1 volumes: - unideal-db_data:/var/lib/mysql app: From f50a1128f3e79f4414a58feeea2d1bdad7579446 Mon Sep 17 00:00:00 2001 From: Alex4386 Date: Mon, 26 May 2025 12:57:45 +0900 Subject: [PATCH 6/6] chore: remove tests --- .../backend/UnidealBackendApplicationTests.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/java/kr/unideal/server/backend/UnidealBackendApplicationTests.java diff --git a/src/test/java/kr/unideal/server/backend/UnidealBackendApplicationTests.java b/src/test/java/kr/unideal/server/backend/UnidealBackendApplicationTests.java deleted file mode 100644 index 31c8f3a..0000000 --- a/src/test/java/kr/unideal/server/backend/UnidealBackendApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package kr.unideal.server.backend; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class UnidealBackendApplicationTests { - - @Test - void contextLoads() { - } - -}