diff --git a/backend/backend/src/main/resources/images/30d65b76-54cb-42ad-a53e-04691c6401ba_testimg3.jpg b/backend/backend/src/main/resources/images/0a51d4af-2922-4057-81cd-756e62ddd13c_testimg1.jpg similarity index 100% rename from backend/backend/src/main/resources/images/30d65b76-54cb-42ad-a53e-04691c6401ba_testimg3.jpg rename to backend/backend/src/main/resources/images/0a51d4af-2922-4057-81cd-756e62ddd13c_testimg1.jpg diff --git a/backend/backend/src/main/resources/images/427dc7fc-d22e-43a3-8fde-69bace73ee6a_lectureImage.jpg b/backend/backend/src/main/resources/images/0bef3f29-11a6-4026-82fb-5a1d0beb9130_testImg2.jpg similarity index 100% rename from backend/backend/src/main/resources/images/427dc7fc-d22e-43a3-8fde-69bace73ee6a_lectureImage.jpg rename to backend/backend/src/main/resources/images/0bef3f29-11a6-4026-82fb-5a1d0beb9130_testImg2.jpg diff --git a/backend/backend/src/main/resources/images/45b6ea03-017a-4d3c-9c5c-aa0a2ada53f9_testimg1.jpg b/backend/backend/src/main/resources/images/45b6ea03-017a-4d3c-9c5c-aa0a2ada53f9_testimg1.jpg deleted file mode 100644 index c53ac5e..0000000 Binary files a/backend/backend/src/main/resources/images/45b6ea03-017a-4d3c-9c5c-aa0a2ada53f9_testimg1.jpg and /dev/null differ diff --git a/backend/backend/src/main/resources/images/7fd1090b-af1a-4f48-885c-30bf45d63778_lectureImage.jpg b/backend/backend/src/main/resources/images/7fd1090b-af1a-4f48-885c-30bf45d63778_lectureImage.jpg deleted file mode 100644 index c53ac5e..0000000 Binary files a/backend/backend/src/main/resources/images/7fd1090b-af1a-4f48-885c-30bf45d63778_lectureImage.jpg and /dev/null differ diff --git a/backend/backend/src/main/resources/images/a69e91c4-5108-41ad-96b9-9d25d5353c73_lectureImage.jpg b/backend/backend/src/main/resources/images/a69e91c4-5108-41ad-96b9-9d25d5353c73_lectureImage.jpg deleted file mode 100644 index c53ac5e..0000000 Binary files a/backend/backend/src/main/resources/images/a69e91c4-5108-41ad-96b9-9d25d5353c73_lectureImage.jpg and /dev/null differ diff --git a/backend/build.gradle b/backend/build.gradle index 1daa388..6aad2ae 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.springframework.boot:spring-boot-starter-actuator' - + implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/backend/src/main/java/com/edufocus/edufocus/global/config/PropertiesConfig.java b/backend/src/main/java/com/edufocus/edufocus/global/config/PropertiesConfig.java index eb01984..f0b476c 100644 --- a/backend/src/main/java/com/edufocus/edufocus/global/config/PropertiesConfig.java +++ b/backend/src/main/java/com/edufocus/edufocus/global/config/PropertiesConfig.java @@ -2,6 +2,7 @@ package com.edufocus.edufocus.global.config; import com.edufocus.edufocus.global.properties.ImagePathProperties; +import com.edufocus.edufocus.global.properties.MailProperties; import com.edufocus.edufocus.global.properties.RabbitMQProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -9,7 +10,8 @@ import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties({ RabbitMQProperties.class, - ImagePathProperties.class + ImagePathProperties.class, + MailProperties.class }) public class PropertiesConfig { } diff --git a/backend/src/main/java/com/edufocus/edufocus/global/config/RedisConfig.java b/backend/src/main/java/com/edufocus/edufocus/global/config/RedisConfig.java new file mode 100644 index 0000000..eb7a84f --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/global/config/RedisConfig.java @@ -0,0 +1,22 @@ +package com.edufocus.edufocus.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } +} diff --git a/backend/src/main/java/com/edufocus/edufocus/global/properties/MailProperties.java b/backend/src/main/java/com/edufocus/edufocus/global/properties/MailProperties.java new file mode 100644 index 0000000..6240fdd --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/global/properties/MailProperties.java @@ -0,0 +1,15 @@ +package com.edufocus.edufocus.global.properties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "spring.mail") +public class MailProperties { + private final String host; + private final Integer port; + private final String name; + private final String password; +} diff --git a/backend/src/main/java/com/edufocus/edufocus/mail/controller/MailController.java b/backend/src/main/java/com/edufocus/edufocus/mail/controller/MailController.java new file mode 100644 index 0000000..a6df23d --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/mail/controller/MailController.java @@ -0,0 +1,39 @@ +package com.edufocus.edufocus.mail.controller; + +import com.edufocus.edufocus.mail.service.MailService; +import com.edufocus.edufocus.user.model.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/mail") +@RequiredArgsConstructor +public class MailController { + + private final MailService mailService; + + private final UserService userService; + + @PostMapping("/sendCode") + public ResponseEntity sendMail(@RequestParam String email) { + if (!userService.isEmailExist(email)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + mailService.sendMail(email); + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping("/verify") + public ResponseEntity verifyCode(@RequestParam String code, @RequestParam String email) { + if (!mailService.verifyCode(code, email)) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + return new ResponseEntity<>(HttpStatus.OK); + } + +} diff --git a/backend/src/main/java/com/edufocus/edufocus/mail/service/MailService.java b/backend/src/main/java/com/edufocus/edufocus/mail/service/MailService.java new file mode 100644 index 0000000..1799126 --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/mail/service/MailService.java @@ -0,0 +1,13 @@ +package com.edufocus.edufocus.mail.service; + +import org.springframework.stereotype.Service; + +@Service +public interface MailService { + + void sendMail(String to); + + String createRandomCode(); + + boolean verifyCode(String code, String email); +} diff --git a/backend/src/main/java/com/edufocus/edufocus/mail/service/MailServiceImpl.java b/backend/src/main/java/com/edufocus/edufocus/mail/service/MailServiceImpl.java new file mode 100644 index 0000000..20859de --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/mail/service/MailServiceImpl.java @@ -0,0 +1,69 @@ +package com.edufocus.edufocus.mail.service; + +import com.edufocus.edufocus.redis.util.RedisUtil; +import com.edufocus.edufocus.user.model.entity.vo.User; +import com.edufocus.edufocus.user.model.repository.UserRepository; +import com.edufocus.edufocus.user.model.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +import java.util.NoSuchElementException; +import java.util.Random; + +@Service +@Slf4j +@ToString +@RequiredArgsConstructor +public class MailServiceImpl implements MailService { + + private final JavaMailSender mailSender; + + private final UserRepository userRepository; + + private final UserService userService; + + private final RedisUtil redisUtil; + + @Override + public void sendMail(String email) { + String code = createRandomCode(); + redisUtil.setDataExpire(code, email, 60 * 5L); + + SimpleMailMessage mail = createEmail(email, "[EDUFOCUS] 비밀번호 찾기 안내", code); + mailSender.send(mail); + } + + @Override + public boolean verifyCode(String code, String email) { + String registedEmail = redisUtil.getData(code); + + return registedEmail != null && registedEmail.equals(email); + } + + private SimpleMailMessage createEmail(String to, String title, String code) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject(title); + message.setText("인증번호 6자리입니다 : " + code); + + return message; + } + + @Override + public String createRandomCode() { + StringBuilder sb = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < 3; i++) { + sb.append((char) (random.nextInt(26) + 65)); + } + for (int i = 0; i < 3; i++) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } + +} diff --git a/backend/src/main/java/com/edufocus/edufocus/redis/util/RedisUtil.java b/backend/src/main/java/com/edufocus/edufocus/redis/util/RedisUtil.java new file mode 100644 index 0000000..ad9bf09 --- /dev/null +++ b/backend/src/main/java/com/edufocus/edufocus/redis/util/RedisUtil.java @@ -0,0 +1,36 @@ +package com.edufocus.edufocus.redis.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +public class RedisUtil { + + private final StringRedisTemplate stringRedisTemplate; + + public String getData(String key) { + ValueOperations valueOperations = stringRedisTemplate.opsForValue(); + return valueOperations.get(key); + } + + public void setData(String key, String value) { + ValueOperations valueOperations = stringRedisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + public void setDataExpire(String key, String value, long duration) { + ValueOperations valueOperations = stringRedisTemplate.opsForValue(); + Duration expireDuration = Duration.ofSeconds(duration); + valueOperations.set(key, value, expireDuration); + } + + public void deleteData(String key) { + stringRedisTemplate.delete(key); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/com/edufocus/edufocus/user/config/WebConfiguration.java b/backend/src/main/java/com/edufocus/edufocus/user/config/WebConfiguration.java index 0972d21..5899062 100644 --- a/backend/src/main/java/com/edufocus/edufocus/user/config/WebConfiguration.java +++ b/backend/src/main/java/com/edufocus/edufocus/user/config/WebConfiguration.java @@ -47,6 +47,6 @@ public class WebConfiguration implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") // 모든 경로에 대해 인터셉터 적용 - .excludePathPatterns("/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui/**", "/auth/**", "/board/**", "/user/**", "/lecture/**", "/qna/**", "/quiz/**", "/video/**", "/registration/**", "/report/**"); // 인증 없이 접근 가능한 경로 설정 + .excludePathPatterns("/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui/**", "/auth/**", "/board/**", "/user/**", "/lecture/**", "/qna/**", "/quiz/**", "/video/**", "/registration/**", "/report/**", "/mail/**"); // 인증 없이 접근 가능한 경로 설정 } } \ No newline at end of file diff --git a/backend/src/main/java/com/edufocus/edufocus/user/controller/UserController.java b/backend/src/main/java/com/edufocus/edufocus/user/controller/UserController.java index c077839..2aecbb3 100644 --- a/backend/src/main/java/com/edufocus/edufocus/user/controller/UserController.java +++ b/backend/src/main/java/com/edufocus/edufocus/user/controller/UserController.java @@ -32,9 +32,9 @@ public class UserController { private final JWTUtil jwtUtil; @PostMapping("/join") - public ResponseEntity join(@RequestBody RequestJoinDto requestJoinDto){ + public ResponseEntity join(@RequestBody RequestJoinDto requestJoinDto) { - if(userService.isUserIdExist(requestJoinDto.getUserId())) + if (userService.isUserIdExist(requestJoinDto.getUserId())) return new ResponseEntity<>("아이디가 중복 됐습니다.", HttpStatus.CONFLICT); userService.join(requestJoinDto); @@ -66,6 +66,14 @@ public class UserController { } } + // 비밀번호 찾기를 통한 변경 + @PutMapping("/updateforgottenpassword") + public ResponseEntity updatePassword(@RequestParam long userId, + @RequestParam String newPassword) { + userService.changeForgottenPassword(userId, newPassword); + return new ResponseEntity<>(HttpStatus.OK); + } + @Operation(summary = "로그인", description = "아이디와 비밀번호를 이용하여 로그인 처리.") @PostMapping("/login") public ResponseEntity> login( @@ -83,8 +91,8 @@ public class UserController { userService.saveRefreshToken(loginUser.getId(), refreshToken); - resultMap.put("name",loginUser.getName()); - resultMap.put("role",loginUser.getRole()); + resultMap.put("name", loginUser.getName()); + resultMap.put("role", loginUser.getRole()); resultMap.put("access-token", accessToken); setCookies(response, refreshToken); @@ -108,7 +116,7 @@ public class UserController { @Operation(summary = "Access Token 재발급", description = "만료된 access token 을 재발급 받는다.") @PostMapping("/refresh") - public ResponseEntity refreshToken(HttpServletRequest request,HttpServletResponse response) { + public ResponseEntity refreshToken(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); String token = null; if (cookies != null) { @@ -120,9 +128,9 @@ public class UserController { } } - try{ + try { jwtUtil.checkToken(token); - }catch (Exception e){ + } catch (Exception e) { throw new InvalidTokenException(); } @@ -140,7 +148,7 @@ public class UserController { Map resultMap = new HashMap<>(); resultMap.put("access-token", accessToken); - userService.saveRefreshToken(userId,refreshToken); + userService.saveRefreshToken(userId, refreshToken); setCookies(response, refreshToken); @@ -175,7 +183,7 @@ public class UserController { } - private void setCookies(HttpServletResponse response, String refreshToken){ + private void setCookies(HttpServletResponse response, String refreshToken) { Cookie refreshCookie = new Cookie("refresh-token", refreshToken); refreshCookie.setPath("/"); refreshCookie.setHttpOnly(true); diff --git a/backend/src/main/java/com/edufocus/edufocus/user/model/repository/UserRepository.java b/backend/src/main/java/com/edufocus/edufocus/user/model/repository/UserRepository.java index 10e9f87..6aea378 100644 --- a/backend/src/main/java/com/edufocus/edufocus/user/model/repository/UserRepository.java +++ b/backend/src/main/java/com/edufocus/edufocus/user/model/repository/UserRepository.java @@ -31,4 +31,6 @@ public interface UserRepository extends JpaRepository { Optional findByUserId(String userId); + Optional findByEmail(String email); + } diff --git a/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserService.java b/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserService.java index 7b2d101..0737045 100644 --- a/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserService.java +++ b/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserService.java @@ -6,23 +6,27 @@ import com.edufocus.edufocus.user.model.entity.dto.RequestJoinDto; import com.edufocus.edufocus.user.model.entity.vo.User; public interface UserService { - void join(RequestJoinDto requestJoinDto); + void join(RequestJoinDto requestJoinDto); - User login(User user); + User login(User user); - void saveRefreshToken(Long id, String refreshToken); + void saveRefreshToken(Long id, String refreshToken); - String getRefreshToken(Long id); + String getRefreshToken(Long id); - void deleteRefreshToken(Long id); + void deleteRefreshToken(Long id); - User userInfo(Long id); + User userInfo(Long id); - String getUserName(Long id); + String getUserName(Long id); - void changeUserInfo(InfoDto infoDto,Long id); + void changeUserInfo(InfoDto infoDto, Long id); - void changePassword(PasswordDto passwordDto,Long id); + void changePassword(PasswordDto passwordDto, Long id); - boolean isUserIdExist(String userId); + boolean isUserIdExist(String userId); + + boolean isEmailExist(String email); + + void changeForgottenPassword(Long id, String newPassword); } diff --git a/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserServiceImpl.java b/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserServiceImpl.java index 77fd4ca..9cd9705 100644 --- a/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserServiceImpl.java +++ b/backend/src/main/java/com/edufocus/edufocus/user/model/service/UserServiceImpl.java @@ -3,11 +3,11 @@ package com.edufocus.edufocus.user.model.service; import com.edufocus.edufocus.user.model.entity.dto.InfoDto; import com.edufocus.edufocus.user.model.entity.dto.PasswordDto; -import com.edufocus.edufocus.user.util.PasswordUtils; import com.edufocus.edufocus.user.model.entity.dto.RequestJoinDto; import com.edufocus.edufocus.user.model.entity.vo.User; import com.edufocus.edufocus.user.model.exception.UserException; import com.edufocus.edufocus.user.model.repository.UserRepository; +import com.edufocus.edufocus.user.util.PasswordUtils; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,8 +23,7 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; - public void join(RequestJoinDto requestJoinDto) - { + public void join(RequestJoinDto requestJoinDto) { User user = User.builder() .userId(requestJoinDto.getUserId()) .email(requestJoinDto.getEmail()) @@ -36,7 +35,7 @@ public class UserServiceImpl implements UserService { } - public User login(User user){ + public User login(User user) { Optional findUser = userRepository.findByUserId(user.getUserId()); if (findUser.isEmpty()) { @@ -63,32 +62,29 @@ public class UserServiceImpl implements UserService { } - - - @Override - public String getUserName(Long id){ + public String getUserName(Long id) { return userRepository.findById(id).get().getName(); } @Override - public void changeUserInfo(InfoDto infoDto, Long id){ + public void changeUserInfo(InfoDto infoDto, Long id) { User user = userRepository.findById(id).orElseThrow(IllegalArgumentException::new); if (infoDto.getName() != null) user.setName(infoDto.getName()); - if(infoDto.getEmail()!=null) + if (infoDto.getEmail() != null) user.setEmail(infoDto.getEmail()); userRepository.save(user); -} + } @Override - public void changePassword(PasswordDto passwordDto, Long id){ + public void changePassword(PasswordDto passwordDto, Long id) { User user = userRepository.findById(id).orElse(null); if (user == null) { @@ -114,30 +110,52 @@ public class UserServiceImpl implements UserService { } public String getTempPassword() { - char[] charSet = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + char[] charSet = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; String str = ""; int idx = 0; - for (int i=0; i<10; i++) { + for (int i = 0; i < 10; i++) { idx = (int) (charSet.length * Math.random()); str += charSet[idx]; } return str; } + @Override - public void saveRefreshToken(Long id, String refreshToken){ + public void saveRefreshToken(Long id, String refreshToken) { userRepository.saveRefreshToken(id, refreshToken); } @Override - public String getRefreshToken(Long id){ + public String getRefreshToken(Long id) { return userRepository.getRefreshToken(id); } @Override - public void deleteRefreshToken(Long id){ + public void deleteRefreshToken(Long id) { userRepository.deleteRefreshToken(id); } + @Override + public boolean isEmailExist(String email) { + Optional user = userRepository.findByEmail(email); + + return user.isPresent(); + } + + @Override + public void changeForgottenPassword(Long id, String newPassword) { + User user = userRepository.findById(id).orElse(null); + + if (user == null) { + throw new UserException("User not found"); + } + + // Hash the new password before saving + user.setPassword(PasswordUtils.hashPassword(newPassword)); + userRepository.save(user); + } + + } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index aa22dc0..479d1de 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -29,4 +29,13 @@ spring.rabbitmq.host=${RABBITMQ_HOST} spring.rabbitmq.port=${RABBITMQ_PORT} spring.rabbitmq.username=${RABBITMQ_USERNAME} spring.rabbitmq.password=${RABBITMQ_PASSWORD} -image.path=${IMAGE_PATH} \ No newline at end of file +image.path=${IMAGE_PATH} +spring.mail.host=smtp.gmail.com +spring.mail.port=${MAIL_PORT} +spring.mail.username=${MAIL_NAME} +spring.mail.password=${MAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.timeout=5000 +spring.mail.properties.mail.smtp.starttls.enable=true +spring.data.redis.host=${REDIS_HOST} +spring.data.redis.port=${REDIS_PORT} \ No newline at end of file