diff --git a/be/build.gradle b/be/build.gradle index 44da8a87..4fef763f 100644 --- a/be/build.gradle +++ b/be/build.gradle @@ -1,48 +1,51 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.8' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.1.8' + id 'io.spring.dependency-management' version '1.1.4' } group = 'yeonba' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'io.projectreactor:reactor-test' - - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - - // aws s3 - implementation 'software.amazon.awssdk:aws-sdk-java:2.16.83' - implementation 'software.amazon.awssdk:s3:2.16.83' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + // aws s3 + implementation 'software.amazon.awssdk:aws-sdk-java:2.16.83' + implementation 'software.amazon.awssdk:s3:2.16.83' + + // spring mail + implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.2' } tasks.named('bootBuildImage') { - builder = 'paketobuildpacks/builder-jammy-base:latest' + builder = 'paketobuildpacks/builder-jammy-base:latest' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/be/src/main/java/yeonba/be/config/MailConfig.java b/be/src/main/java/yeonba/be/config/MailConfig.java new file mode 100644 index 00000000..94eeced4 --- /dev/null +++ b/be/src/main/java/yeonba/be/config/MailConfig.java @@ -0,0 +1,40 @@ +package yeonba.be.config; + +import java.util.Properties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfig { + + @Value("${GOOGLE_SMTP_HOST}") + private String serverHost; + + @Value("${GOOGLE_SMTP_PORT}") + private int serverPort; + + @Value("${GOOGLE_SMTP_USERNAME}") + private String username; + + @Value("${GOOGLE_SMTP_PASSWORD}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(serverHost); + mailSender.setPort(serverPort); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties properties = mailSender.getJavaMailProperties(); + properties.put("mail.transport.protocol", "smtp"); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", "true"); + + return mailSender; + } +} diff --git a/be/src/main/java/yeonba/be/login/controller/LoginController.java b/be/src/main/java/yeonba/be/login/controller/LoginController.java index a722d5a7..a2fc39ba 100644 --- a/be/src/main/java/yeonba/be/login/controller/LoginController.java +++ b/be/src/main/java/yeonba/be/login/controller/LoginController.java @@ -3,6 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,107 +19,107 @@ import yeonba.be.login.dto.response.UserJoinResponse; import yeonba.be.login.dto.response.UserLoginResponse; import yeonba.be.login.dto.response.UserRefreshTokenResponse; +import yeonba.be.login.service.LoginService; import yeonba.be.util.CustomResponse; @Tag(name = "Login", description = "로그인 관련 API") @RestController +@RequiredArgsConstructor public class LoginController { - @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") - @PostMapping("/users/join") - public ResponseEntity> join( - @RequestBody UserJoinRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserJoinResponse(createdJwt))); - } - - @Operation( - summary = "전화번호 인증 코드 전송", - description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다." - ) - @ApiResponse( - responseCode = "204", - description = "전화번호 인증 코드 전송 성공" - ) - @PostMapping("/users/help/id-inquiry/verification-code") - public ResponseEntity> verifyPhoneNumber( - @RequestBody UserPhoneNumberVerifyRequest request) { - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation( - summary = "아이디 찾기", - description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다." - ) - @ApiResponse( - responseCode = "200", - description = "아이디 찾기 정상 처리" - ) - @PostMapping("/users/help/id-inquiry") - public ResponseEntity> idInquiry( - @RequestBody UserIdInquiryRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); - } - - @Operation( - summary = "비밀번호 찾기", - description = "이메일로 임시 비밀번호를 발급받을 수 있습니다." - ) - @ApiResponse( - responseCode = "204", - description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리" - ) - @PostMapping("/users/help/pw-inquiry") - public ResponseEntity> passwordInquiry( - @RequestBody UserPasswordInquiryRequest request) { - - return ResponseEntity - .accepted() - .body(new CustomResponse<>()); - } - - @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") - @PostMapping("/users/login") - public ResponseEntity> login( - @RequestBody UserLoginRequest request) { - - return ResponseEntity - .ok() - .body(new CustomResponse<>( - new UserLoginResponse( - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", - """ - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ - .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" - ))); - } - - @Operation( - summary = "access token 재발급", - description = "refresh token을 통해 access token을 재발급받을 수 있습니다." - ) - @PostMapping("/users/refresh") - public ResponseEntity> refresh( - @RequestBody UserRefreshTokenRequest request) { - - String createdJwt = "created"; - - return ResponseEntity - .ok() - .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); - } + private final LoginService loginService; + + @Operation(summary = "회원가입", description = "회원가입을 할 수 있습니다.") + @PostMapping("/users/join") + public ResponseEntity> join( + @RequestBody UserJoinRequest request) { + + String createdJwt = "created"; + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserJoinResponse(createdJwt))); + } + + @Operation( + summary = "전화번호 인증 코드 전송", + description = "전화번호 인증을 위해 해당 번호로 인증 코드를 발송합니다." + ) + @ApiResponse( + responseCode = "204", + description = "전화번호 인증 코드 전송 성공" + ) + @PostMapping("/users/help/id-inquiry/verification-code") + public ResponseEntity> verifyPhoneNumber( + @RequestBody UserPhoneNumberVerifyRequest request) { + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation( + summary = "아이디 찾기", + description = "인증 코드를 바탕으로 아이디를 찾을 수 있습니다." + ) + @ApiResponse( + responseCode = "200", + description = "아이디 찾기 정상 처리" + ) + @PostMapping("/users/help/id-inquiry") + public ResponseEntity> idInquiry( + @RequestBody UserIdInquiryRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserIdInquiryResponse("mj3242@naver.com"))); + } + + @Operation(summary = "비밀번호 찾기", description = "이메일로 임시 비밀번호를 발급받을 수 있습니다.") + @ApiResponse(responseCode = "202", description = "임시 비밀번호 발급(비밀번호 찾기) 정상 처리") + @PostMapping("/users/help/pw-inquiry") + public ResponseEntity> passwordInquiry( + @Valid @RequestBody UserPasswordInquiryRequest request) { + + loginService.sendTemporaryPasswordMail(request); + + return ResponseEntity + .accepted() + .body(new CustomResponse<>()); + } + + @Operation(summary = "로그인", description = "로그인을 할 수 있습니다.") + @PostMapping("/users/login") + public ResponseEntity> login( + @RequestBody UserLoginRequest request) { + + return ResponseEntity + .ok() + .body(new CustomResponse<>( + new UserLoginResponse( + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""", + """ + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c""" + ))); + } + + @Operation( + summary = "access token 재발급", + description = "refresh token을 통해 access token을 재발급받을 수 있습니다." + ) + @PostMapping("/users/refresh") + public ResponseEntity> refresh( + @RequestBody UserRefreshTokenRequest request) { + + String createdJwt = "created"; + + return ResponseEntity + .ok() + .body(new CustomResponse<>(new UserRefreshTokenResponse(createdJwt))); + } } diff --git a/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java b/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java index 14caf0db..a140a261 100644 --- a/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java +++ b/be/src/main/java/yeonba/be/login/dto/request/UserPasswordInquiryRequest.java @@ -1,17 +1,19 @@ package yeonba.be.login.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Pattern; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter -@AllArgsConstructor +@NoArgsConstructor public class UserPasswordInquiryRequest { - @Schema( - type = "string", - description = "임시 비밀번호를 받을 이메일", - example = "mj3242@naver.com" - ) - private String email; + @Schema(type = "string", + description = "임시 비밀번호를 받을 이메일", + example = "mj3242@naver.com") + @Pattern( + regexp = "[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$", + message = "유효하지 않은 이메일 형식입니다.") + private String email; } diff --git a/be/src/main/java/yeonba/be/login/service/LoginService.java b/be/src/main/java/yeonba/be/login/service/LoginService.java new file mode 100644 index 00000000..73a7d2fd --- /dev/null +++ b/be/src/main/java/yeonba/be/login/service/LoginService.java @@ -0,0 +1,45 @@ +package yeonba.be.login.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import yeonba.be.login.dto.request.UserPasswordInquiryRequest; +import yeonba.be.user.entity.User; +import yeonba.be.user.repository.UserQuery; +import yeonba.be.util.EmailService; +import yeonba.be.util.TemporaryPasswordGenerator; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final String TEMPORARY_PASSWORD_EMAIL_SUBJECT = "연바(연애는 바로 지금) 임시비밀번호 발급"; + private final String TEMPORARY_PASSWORD_EMAIL_TEXT = "임시비밀번호 : %s"; + private final UserQuery userQuery; + private final EmailService emailService; + + /* + 임시 비밀번호는 다음 과정을 거친다. + 1. 요청 이메일 기반 사용자 조회 + 2. 임시 비밀번호 생성 + 3. 사용자 비밀번호, 임시 비밀번호로 변경 + 4. 임시 비밀번호 발급 메일 전송 + */ + + // TODO : 비밀번호 암호화 로직 추가 + + @Transactional + public void sendTemporaryPasswordMail(UserPasswordInquiryRequest request) { + + String email = request.getEmail(); + User user = userQuery.findByEmail(email); + + String temporaryPassword = TemporaryPasswordGenerator.generatePassword(); + + String encryptedPassword = temporaryPassword; + user.changePassword(encryptedPassword); + + String text = String.format(TEMPORARY_PASSWORD_EMAIL_TEXT, temporaryPassword); + emailService.sendMail(email, TEMPORARY_PASSWORD_EMAIL_SUBJECT, text); + } +} diff --git a/be/src/main/java/yeonba/be/user/entity/User.java b/be/src/main/java/yeonba/be/user/entity/User.java index 0591b923..11b7e6fb 100644 --- a/be/src/main/java/yeonba/be/user/entity/User.java +++ b/be/src/main/java/yeonba/be/user/entity/User.java @@ -32,186 +32,186 @@ @EqualsAndHashCode(of = "id") public class User { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private boolean gender; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String nickname; - - @Column(nullable = false) - private LocalDate birth; - private int age; - private int height; - - @Column(nullable = false) - private String email; - - @Column(nullable = false) - private String encryptedPassword; - - @Column(nullable = false) - private String salt; - - @Column(nullable = false) - private String phoneNumber; - private int arrow; - private int photoSyncRate; - private boolean inactiveStatus; - - @Column(nullable = false) - private String bodyType; - - @Column(nullable = false) - private String job; - - @Column(nullable = false) - private String mbti; - - @ManyToOne - @JoinColumn(name = "vocal_range_id") - private VocalRange vocalRange; - - @ManyToOne - @JoinColumn(name = "animal_id") - private Animal animal; - - @ManyToOne - @JoinColumn(name = "area_id") - private Area area; - - @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) - List profilePhotos; - - private LocalDateTime lastAccessedAt; - - @CreatedDate - private LocalDateTime createdAt; - - @LastModifiedDate - private LocalDateTime updatedAt; - - private LocalDateTime deletedAt; - - public User( - boolean gender, - String name, - String nickname, - LocalDate birth, - int age, - int height, - String email, - String encryptedPassword, - String salt, - String phoneNumber, - int arrow, - int photoSyncRate, - String bodyType, - String job, - String mbti, - VocalRange vocalRange, - Animal animal, - Area area, - List profilePhotos) { - this.gender = gender; - this.name = name; - this.nickname = nickname; - this.birth = birth; - this.age = age; - this.height = height; - this.email = email; - this.encryptedPassword = encryptedPassword; - this.salt = salt; - this.phoneNumber = phoneNumber; - this.arrow = arrow; - this.photoSyncRate = photoSyncRate; - this.inactiveStatus = true; - this.bodyType = bodyType; - this.job = job; - this.mbti = mbti; - this.vocalRange = vocalRange; - this.animal = animal; - this.area = area; - this.profilePhotos = profilePhotos; - } - - public void validateSameUser(User user) { - - if (!this.equals(user)) { - throw new IllegalArgumentException("동일한 사용자가 아닙니다."); - } - } - - public void validateNotSameUser(User user) { - - if (this.equals(user)) { - throw new IllegalArgumentException("동일한 사용자입니다."); - } - } - - public void changePassword(String encryptedNewPassword) { - - this.encryptedPassword = encryptedNewPassword; - } - - /** - * 삭제된 사용자인지 검증 - */ - public void validateDeletedUser(LocalDateTime now) { - - if (this.deletedAt.isAfter(now)) { - throw new IllegalArgumentException("삭제된 사용자입니다."); - } - } - - public void validateDailyCheck(LocalDate dailyCheckDay) { - - if (this.lastAccessedAt.isAfter(dailyCheckDay.atStartOfDay())) { - throw new GeneralException(ArrowException.ALREADY_CHECKED_USER); - } - } - - public String getRepresentativeProfilePhoto() { - - return this.profilePhotos.get(0).getPhotoUrl(); - } - - public void updateLastAccessedAt(LocalDateTime accessedAt) { - - this.lastAccessedAt = accessedAt; - } - - public void plusArrow(int arrow) { - - this.arrow += arrow; - } - - public void minusArrow(int arrow) { - - if (this.arrow < arrow) { - throw new GeneralException(ArrowException.NOT_ENOUGH_ARROW_TO_SEND); - } - - this.arrow -= arrow; - } - - public String getGender() { - if (this.gender) { - - return "남"; - } - - return "여"; - } - - public List getProfilePhotoUrls() { - - return this.profilePhotos.stream() - .map(ProfilePhoto::getPhotoUrl) - .toList(); - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private boolean gender; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private LocalDate birth; + private int age; + private int height; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String encryptedPassword; + + @Column(nullable = false) + private String salt; + + @Column(nullable = false) + private String phoneNumber; + private int arrow; + private int photoSyncRate; + private boolean inactive; + + @Column(nullable = false) + private String bodyType; + + @Column(nullable = false) + private String job; + + @Column(nullable = false) + private String mbti; + + @ManyToOne + @JoinColumn(name = "vocal_range_id") + private VocalRange vocalRange; + + @ManyToOne + @JoinColumn(name = "animal_id") + private Animal animal; + + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; + + @OneToMany(mappedBy = "user", fetch = FetchType.EAGER) + List profilePhotos; + + private LocalDateTime lastAccessedAt; + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + private LocalDateTime deletedAt; + + public User( + boolean gender, + String name, + String nickname, + LocalDate birth, + int age, + int height, + String email, + String encryptedPassword, + String salt, + String phoneNumber, + int arrow, + int photoSyncRate, + String bodyType, + String job, + String mbti, + VocalRange vocalRange, + Animal animal, + Area area, + List profilePhotos) { + this.gender = gender; + this.name = name; + this.nickname = nickname; + this.birth = birth; + this.age = age; + this.height = height; + this.email = email; + this.encryptedPassword = encryptedPassword; + this.salt = salt; + this.phoneNumber = phoneNumber; + this.arrow = arrow; + this.photoSyncRate = photoSyncRate; + this.inactive = true; + this.bodyType = bodyType; + this.job = job; + this.mbti = mbti; + this.vocalRange = vocalRange; + this.animal = animal; + this.area = area; + this.profilePhotos = profilePhotos; + } + + public void validateSameUser(User user) { + + if (!this.equals(user)) { + throw new IllegalArgumentException("동일한 사용자가 아닙니다."); + } + } + + public void validateNotSameUser(User user) { + + if (this.equals(user)) { + throw new IllegalArgumentException("동일한 사용자입니다."); + } + } + + public void changePassword(String encryptedNewPassword) { + + this.encryptedPassword = encryptedNewPassword; + } + + /** + * 삭제된 사용자인지 검증 + */ + public void validateDeletedUser(LocalDateTime now) { + + if (this.deletedAt.isAfter(now)) { + throw new IllegalArgumentException("삭제된 사용자입니다."); + } + } + + public void validateDailyCheck(LocalDate dailyCheckDay) { + + if (this.lastAccessedAt.isAfter(dailyCheckDay.atStartOfDay())) { + throw new GeneralException(ArrowException.ALREADY_CHECKED_USER); + } + } + + public String getRepresentativeProfilePhoto() { + + return this.profilePhotos.get(0).getPhotoUrl(); + } + + public void updateLastAccessedAt(LocalDateTime accessedAt) { + + this.lastAccessedAt = accessedAt; + } + + public void plusArrow(int arrow) { + + this.arrow += arrow; + } + + public void minusArrow(int arrow) { + + if (this.arrow < arrow) { + throw new GeneralException(ArrowException.NOT_ENOUGH_ARROW_TO_SEND); + } + + this.arrow -= arrow; + } + + public String getGender() { + if (this.gender) { + + return "남"; + } + + return "여"; + } + + public List getProfilePhotoUrls() { + + return this.profilePhotos.stream() + .map(ProfilePhoto::getPhotoUrl) + .toList(); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/UserQuery.java b/be/src/main/java/yeonba/be/user/repository/UserQuery.java index 9d32a736..dedc56da 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserQuery.java +++ b/be/src/main/java/yeonba/be/user/repository/UserQuery.java @@ -10,11 +10,17 @@ @RequiredArgsConstructor public class UserQuery { - private final UserRepository userRepository; + private final UserRepository userRepository; - public User findById(long userId) { + public User findById(long userId) { - return userRepository.findById(userId) - .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); - } + return userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } + + public User findByEmail(String email) { + + return userRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(UserException.USER_NOT_FOUND)); + } } diff --git a/be/src/main/java/yeonba/be/user/repository/UserRepository.java b/be/src/main/java/yeonba/be/user/repository/UserRepository.java index cb9c5ddb..7d3a98b2 100644 --- a/be/src/main/java/yeonba/be/user/repository/UserRepository.java +++ b/be/src/main/java/yeonba/be/user/repository/UserRepository.java @@ -1,5 +1,6 @@ package yeonba.be.user.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import yeonba.be.user.entity.User; @@ -7,4 +8,5 @@ @Repository public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/be/src/main/java/yeonba/be/util/EmailService.java b/be/src/main/java/yeonba/be/util/EmailService.java new file mode 100644 index 00000000..484164f9 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/EmailService.java @@ -0,0 +1,30 @@ +package yeonba.be.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailService { + + @Value("${GOOGLE_SMTP_USERNAME}") + private String serviceEmail; + + private final JavaMailSender mailSender; + + public void sendMail( + String to, + String subject, + String text) { + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(serviceEmail); + mailMessage.setTo(to); + mailMessage.setSubject(subject); + mailMessage.setText(text); + + mailSender.send(mailMessage); + } +} diff --git a/be/src/main/java/yeonba/be/util/GlobalValidationRegex.java b/be/src/main/java/yeonba/be/util/GlobalValidationRegex.java new file mode 100644 index 00000000..b496a24e --- /dev/null +++ b/be/src/main/java/yeonba/be/util/GlobalValidationRegex.java @@ -0,0 +1,18 @@ +package yeonba.be.util; + +public enum GlobalValidationRegex { + + EMAIL("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"), + PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$"); + + private final String pattern; + + GlobalValidationRegex(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + + return pattern; + } +} diff --git a/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java b/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java new file mode 100644 index 00000000..2d396b38 --- /dev/null +++ b/be/src/main/java/yeonba/be/util/TemporaryPasswordGenerator.java @@ -0,0 +1,48 @@ +package yeonba.be.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; + +public class TemporaryPasswordGenerator { + + private static final int MIN_LENGTH = 8; + + private static final String DIGITS = "0123456789"; + private static final String LOWER_CASES = "abcdefghijklmnopqrstuvwxyz"; + private static final String SPECIAL_CHARS = "~#@!"; + private static final String UPPER_CASES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + public static String generatePassword() { + + // 각 카테고리에서 최소 한 문자씩 선택 + StringBuilder password = new StringBuilder(); + password.append(RandomStringUtils.random(1, LOWER_CASES)) + .append(RandomStringUtils.random(1, UPPER_CASES)) + .append(RandomStringUtils.random(1, DIGITS)) + .append(RandomStringUtils.random(1, SPECIAL_CHARS)); + + // 모든 가능한 문자를 포함하는 문자열 + String allPossibleChars = DIGITS.concat(LOWER_CASES) + .concat(SPECIAL_CHARS) + .concat(UPPER_CASES); + + // 나머지 길이 채우기 + password.append(RandomStringUtils.random(MIN_LENGTH - 4, allPossibleChars)); + + // 문자열 섞기 + List passwordChars = new ArrayList<>( + password.chars() + .mapToObj(c -> (char) c) + .toList()); + Collections.shuffle(passwordChars); + + // 최종 임시 비밀번호 생성 및 반환 + + return passwordChars.stream() + .map(String::valueOf) + .collect(Collectors.joining()); + } +} diff --git a/be/src/main/resources/application.yml b/be/src/main/resources/application.yml index f2ba3c03..7ce0b78e 100644 --- a/be/src/main/resources/application.yml +++ b/be/src/main/resources/application.yml @@ -1,13 +1,15 @@ spring: config: import: "classpath:application.properties" + datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + jpa: properties: hibernate: show_sql: true - format_sql: true + format_sql: true \ No newline at end of file