Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
application-secret.yml
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ dependencies {
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

implementation 'org.commonmark:commonmark:0.21.0'

implementation 'org.springframework.boot:spring-boot-starter-mail'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableJpaAuditing // @EntityListeners(AuditingEntityListener.class) 가 작동하도록 허용
@EnableAsync // 비동기 기능 활성화
public class SpringAdditionalApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public void run(String... args) throws Exception {

SiteUser user1 = userService.create("user1", "user1@test.com", "1234");
SiteUser user2 = userService.create("user2", "user2@test.com", "1234");
SiteUser user3 = userService.create("puar12", "r4560798@naver.com", "1234");

for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ll.spring_additional.boundedContext.user.Form;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PWChangeForm {
@NotBlank(message = "기존 비밀번호는 필수항목입니다.")
private String prePassword;

@NotBlank(message = "새 비밀번호는 필수항목입니다.")
private String newPassword1;

@NotBlank(message = "새 비밀번호 확인은 필수항목입니다.")
private String newPassword2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ll.spring_additional.boundedContext.user.Form;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserPWFindForm {
@Size(min = 3, max = 25)
@NotBlank(message = "사용자ID는 필수항목입니다.")
private String username;

@NotBlank(message = "이메일은 필수항목입니다.")
@Email
private String email;
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
package com.ll.spring_additional.boundedContext.user.controller;

import java.security.Principal;
import java.util.List;

import jakarta.validation.Valid;

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.ll.spring_additional.base.exception.DataNotFoundException;
import com.ll.spring_additional.boundedContext.answer.entity.Answer;
import com.ll.spring_additional.boundedContext.answer.service.AnswerService;
import com.ll.spring_additional.boundedContext.question.entity.Question;
import com.ll.spring_additional.boundedContext.question.service.QuestionService;
import com.ll.spring_additional.boundedContext.user.Form.PWChangeForm;
import com.ll.spring_additional.boundedContext.user.Form.UserCreateForm;
import com.ll.spring_additional.boundedContext.user.Form.UserPWFindForm;
import com.ll.spring_additional.boundedContext.user.entity.SiteUser;
import com.ll.spring_additional.boundedContext.user.service.UserService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {

private final UserService userService;

private final QuestionService questionService;

private final AnswerService answerService;

private final PasswordEncoder passwordEncoder;

@GetMapping("/login")
public String login() {
return "user/login_form";
Expand All @@ -60,11 +66,11 @@ public String signup(@Valid UserCreateForm userCreateForm, BindingResult binding
try {
userService.create(userCreateForm.getUsername(),
userCreateForm.getEmail(), userCreateForm.getPassword1());
}catch(DataIntegrityViolationException e) {
} catch (DataIntegrityViolationException e) {
e.printStackTrace();
bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
return "user/signup_form";
}catch(Exception e) {
} catch (Exception e) {
e.printStackTrace();
bindingResult.reject("signupFailed", e.getMessage());
return "user/signup_form";
Expand All @@ -74,11 +80,10 @@ public String signup(@Valid UserCreateForm userCreateForm, BindingResult binding

@GetMapping("/mypage")
@PreAuthorize("isAuthenticated()")
public String showmyPage(Model model, Principal principal)
{
public String showmyPage(Model model, Principal principal) {
SiteUser user = userService.getUser(principal.getName());

if(user == null) {
if (user == null) {
throw new DataNotFoundException("사용자를 찾을 수 없습니다.");
}
model.addAttribute("user", user);
Expand All @@ -97,4 +102,77 @@ public String showmyPage(Model model, Principal principal)

return "user/my_page";
}

@PreAuthorize("isAnonymous()")
@GetMapping("/pw_find")
public String showFindPassWord(UserPWFindForm userPWFindForm) {
return "user/pw_find";
}

@PreAuthorize("isAnonymous()")
@PostMapping("/pw_find")
public String findPassWord(Model model, @Valid UserPWFindForm userPWFindForm, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "user/pw_find";
}

SiteUser user = userService.getUser(userPWFindForm.getUsername());

if (user == null) {
bindingResult.reject("notFindUser", "일치하는 사용자가 없습니다.");
return "user/pw_find";
}

if (!user.getEmail().equals(userPWFindForm.getEmail())) {
bindingResult.reject("notCorrectEmail", "등록된 회원 정보와 이메일이 다릅니다.");
return "user/pw_find";
}

String tempPW = userService.setTemporaryPW(user);

// 이메일 전송
// @Async 붙은 메서드는 동일한 클래스에서 호출할 수 없기에 컨트롤러에서 메일 발송 요청
userService.sendEmail(userPWFindForm.getEmail(), user.getUsername(), tempPW);

// 로그인 페이지에서 보여줄 성공 메시지를 플래시 애트리뷰트로 추가
redirectAttributes.addFlashAttribute("successMessage", "임시 비밀번호가 이메일로 전송되었습니다. 이메일 확인 후 로그인 해주세요.");

return "redirect:/user/login";
}

@PreAuthorize("isAuthenticated()")
@GetMapping("/change/passwd")
public String showChangePW(@ModelAttribute("pwChangeForm") PWChangeForm pwChangeForm) {
return "user/pw_change";
}

@PreAuthorize("isAuthenticated()")
@PostMapping("/change/passwd")
public String changePW(@Valid @ModelAttribute("pwChangeForm") PWChangeForm pwChangeForm, BindingResult bindingResult, Model model,
Principal principal, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "user/pw_change";
}

SiteUser user = userService.getUser(principal.getName());

// 이전 패스워드와 맞지 않을경우
if (!passwordEncoder.matches(pwChangeForm.getPrePassword(), user.getPassword())) {
bindingResult.reject("notMatchPW", "이전 비밀번호가 일치하지 않습니다.");
return "user/pw_change";
}
// 새 비밀번호, 비밀번호 확인 창 일치하지 않을경우
if (!pwChangeForm.getNewPassword1().equals(pwChangeForm.getNewPassword2())) {
bindingResult.reject("notMatchNewPW", "새 비밀번호와 확인이 일치하지 않습니다.");
return "user/pw_change";
}

userService.updatePassWord(user, pwChangeForm.getNewPassword1());

// 로그인 페이지에서 보여줄 성공 메시지를 플래시 애트리뷰트로 추가
redirectAttributes.addFlashAttribute("successMessage", "비밀번호 변경 성공!");

return "redirect:/user/mypage";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.ll.spring_additional.boundedContext.user.UserRole;
import com.ll.spring_additional.boundedContext.user.userRole.UserRole;
import com.ll.spring_additional.boundedContext.user.entity.SiteUser;
import com.ll.spring_additional.boundedContext.user.repository.UserRepository;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.ll.spring_additional.boundedContext.user.service;

import java.util.Optional;
import java.util.Random;
import java.util.concurrent.Executor;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -21,6 +26,12 @@ public class UserService {

private final PasswordEncoder passwordEncoder;

private final JavaMailSender mailSender;

private static final String ADMIN_ADDRESS = "r4560798@naver.com";

// private final Executor executor;

@Transactional
public SiteUser create(String username, String email, String password) {
SiteUser user = new SiteUser();
Expand All @@ -39,4 +50,45 @@ public SiteUser getUser(String username) {
throw new DataNotFoundException("siteuser not found");
}
}

@Transactional
public String setTemporaryPW(SiteUser user) {
// 임시 비밀번호 생성 및 암호화
String temporaryPassword = createRandomPassword();
user.setPassword(passwordEncoder.encode(temporaryPassword));
userRepository.save(user);
return temporaryPassword;
}

@Async // 비동기
public void sendEmail(String email, String userName, String tempPW) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setFrom(ADMIN_ADDRESS);
message.setSubject(userName+"님의 임시비밀번호 안내 메일입니다.");
message.setText("안녕하세요 "+userName+"님의 임시 비밀번호는 [" + tempPW +"] 입니다.");

mailSender.send(message);

}

private String createRandomPassword() {
int leftLimit = 48; // numeral '0'
int rightLimit = 122; // letter 'z'
int targetStringLength = 10;
Random random = new Random();
String generatedString = random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();

return generatedString;
}

@Transactional
public void updatePassWord(SiteUser user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.ll.spring_additional.boundedContext.user;
package com.ll.spring_additional.boundedContext.user.userRole;

import lombok.Getter;

Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/application-secret.yml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
spring:
mail:
host: smtp.naver.com
port: 465
username: 이메일@naver.com
password: 비밀번호
properties:
mail.smtp.auth: true
mail.smtp.ssl.enable: true
mail.smtp.ssl.trust: smtp.naver.com
mail.smtp.starttls.enable: true
2 changes: 1 addition & 1 deletion src/main/resources/templates/common/form_errors.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div th:fragment="formErrorsFragment" class="alert alert-danger"
role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}" />
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"> </div>
</div>
4 changes: 4 additions & 0 deletions src/main/resources/templates/user/login_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
사용자ID 또는 비밀번호를 확인해 주세요.
</div>
</div>
<div th:if="${successMessage !=null}"} class="alert alert-success" th:text="${successMessage}"></div>
<div class="mb-3">
<label for="username" class="form-label">사용자ID</label>
<input type="text" name="username" id="username" class="form-control">
Expand All @@ -17,5 +18,8 @@
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
<a href="/user/pw_find" style="display:block; margin-top:15px;">
<button class="btn btn-primary">비밀번호 찾기</button>
</a>
</main>
</html>
6 changes: 6 additions & 0 deletions src/main/resources/templates/user/my_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
</head>
<body>
<main layout:fragment="content" class="container gap-3" style="width: 700px;">
<div th:if="${successMessage !=null}"} class="alert alert-success" th:text="${successMessage}" style="text-align: center; margin-top:10px"></div>
<div class="card d-flex flex-column align-items-center shadow p-3 mb-5 bg-body rounded" style="margin-top : 10px;">
<div class="card-body gap-3">
<div class="d-flex align-items-baseline justify-content-center gap-2">
Expand All @@ -23,6 +24,11 @@
<p>e-mail :</p>
<p th:text="${user.email}"></p>
</div>
<div class="d-flex gap-1 align-items-baseline justify-content-center container-fluid">
<a href="/user/change/passwd" class="btn btn-outline-primary col-12 d-flex align-items-center justify-content-center">
<span>비밀번호 변경</span>
</a>
</div>
</div>
</div>

Expand Down
Loading