Skip to content

Commit

Permalink
Feature/#109 member 관련 api 구현 (#161)
Browse files Browse the repository at this point in the history
* feat: member table 컬럼에 is_authorized 항목 추가

#109

* feat: 추가적인 domain 정의

#109

* feat: 메일, 학번, 전화번호 중복 확인 service 생성

#109

* feat: 회원가입 request dto 구현

#109

* feat: springframework security 의존성 추가

#109

* feat: 회원가입 구현

#109

* feat: 회원가입 테스트 코드 작성

#109

* feat: backend config 폴더 submodule로 가져오기

#109

* feat: submodule 경로 수정을 위한 삭제

#109

* feat: config 서브모듈 추가

#109

* feat: 인증코드 발송 api 와 test 코드 구현

#109

* feat: 변경된 이메일 인증 로직에 의한 table schema 수정

#109

* feat: 중복하는 이메일, 학번, 전화번호인지 확인할 수 있는 api 구현

#109

* refact requestFields의 속성들 snake 문법으로 수정

#109

* feat: 로그인 api 구현

#109

* feat: 임시 비밀번호 발급 api 구현

#109

* feat: ArgumentResolver 방식으로 jwtToken 발급

#109

* feat: 로그인과 임시 비밀번호 발급 test 코드 작성

#109

* feat: 비밀번호 수정 api 구현

#109

* feat: 비밀번호 수정 test 코드 작성

#109

* feat: 테스트 코드 문서화 작업

#109

* feat: config 파일 업데이트

#109

* feat: submodule true 설정

#109

* feat: submodule 관련 설정 추가

#109

* feat: 코드리뷰 반영

#109
  • Loading branch information
llddang authored Aug 22, 2024
1 parent d02e09f commit 5974799
Show file tree
Hide file tree
Showing 50 changed files with 2,843 additions and 160 deletions.
8 changes: 8 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,21 @@ 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-security'
implementation 'org.apache.poi:poi:5.2.3'
implementation 'org.apache.poi:poi-ooxml:5.2.3'
implementation 'commons-io:commons-io'
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// restdocs
Expand Down
106 changes: 106 additions & 0 deletions backend/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ include::{snippets}/student-find-all/http-response.adoc[]
.Response Body
include::{snippets}/student-find-all/response-fields.adoc[]

=== `PATCH`: 비밀번호 수정

.HTTP Request
include::{snippets}/member-change-password/http-request.adoc[]

.Request Body
include::{snippets}/member-change-password/request-fields.adoc[]

.HTTP Response
include::{snippets}/member-change-password/http-response.adoc[]

== 학과

=== `GET`: 단과대학 목록 조회
Expand Down Expand Up @@ -222,3 +233,98 @@ include::{snippets}/download-score-file/query-parameters.adoc[]

.HTTP Response
include::{snippets}/download-score-file/http-response.adoc[]

== 인증

=== `POST`: 회원가입

.HTTP Request
include::{snippets}/auth-sign-up/http-request.adoc[]

.Request Body
include::{snippets}/auth-sign-up/request-fields.adoc[]

.HTTP Response
include::{snippets}/auth-sign-up/http-response.adoc[]

=== `POST`: 부산대 메일 인증 코드 발송

.HTTP Request
include::{snippets}/auth-send-auth-code/http-request.adoc[]

.Request Body
include::{snippets}/auth-send-auth-code/request-fields.adoc[]

.HTTP Response
include::{snippets}/auth-send-auth-code/http-response.adoc[]

.Response Body
include::{snippets}/auth-send-auth-code/response-fields.adoc[]

=== `GET`: 이메일 중복 확인

.HTTP Request
include::{snippets}/auth-check-duplicate-email/http-request.adoc[]

.Path Parameters
include::{snippets}/auth-check-duplicate-email/query-parameters.adoc[]

.HTTP Response
include::{snippets}/auth-check-duplicate-email/http-response.adoc[]

.Response Body
include::{snippets}/auth-check-duplicate-email/response-fields.adoc[]

=== `GET`: 학번 중복 확인

.HTTP Request
include::{snippets}/auth-check-duplicate-student-id/http-request.adoc[]

.Path Parameters
include::{snippets}/auth-check-duplicate-student-id/query-parameters.adoc[]

.HTTP Response
include::{snippets}/auth-check-duplicate-student-id/http-response.adoc[]

.Response Body
include::{snippets}/auth-check-duplicate-student-id/response-fields.adoc[]

=== `GET`: 전화번호 중복 확인

.HTTP Request
include::{snippets}/auth-check-duplicate-phone-number/http-request.adoc[]

.Path Parameters
include::{snippets}/auth-check-duplicate-phone-number/query-parameters.adoc[]

.HTTP Response
include::{snippets}/auth-check-duplicate-phone-number/http-response.adoc[]

.Response Body
include::{snippets}/auth-check-duplicate-phone-number/response-fields.adoc[]

=== `POST`: 로그인

.HTTP Request
include::{snippets}/auth-sign-in/http-request.adoc[]

.Request Body
include::{snippets}/auth-sign-in/request-fields.adoc[]

.HTTP Response
include::{snippets}/auth-sign-in/http-response.adoc[]

.Response Body
include::{snippets}/auth-sign-in/response-fields.adoc[]

=== `PATCH`: 임시 비밀번호 발급

.HTTP Request
include::{snippets}/auth-reset-password/http-request.adoc[]

.Request Body
include::{snippets}/auth-reset-password/request-fields.adoc[]

.HTTP Response
include::{snippets}/auth-reset-password/http-response.adoc[]

9 changes: 5 additions & 4 deletions backend/src/main/java/sw_css/SwCssApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class SwCssApplication {

public static void main(String[] args) {
SpringApplication.run(SwCssApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(SwCssApplication.class, args);
}

}
36 changes: 36 additions & 0 deletions backend/src/main/java/sw_css/auth/api/SignInController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sw_css.auth.api;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sw_css.auth.application.AuthSignInService;
import sw_css.auth.application.dto.request.ResetPasswordRequest;
import sw_css.auth.application.dto.request.SignInRequest;
import sw_css.auth.application.dto.response.SignInResponse;

@Validated
@RequestMapping("/sign-in")
@RestController
@RequiredArgsConstructor
public class SignInController {

private final AuthSignInService authSignInService;

@PostMapping
public ResponseEntity<SignInResponse> signIn(@RequestBody @Valid SignInRequest request) {
return ResponseEntity.ok(authSignInService.signIn(request.email(), request.password()));
}

@PatchMapping("/reset-password")
public ResponseEntity<Void> resetPassword(
@RequestBody @Valid ResetPasswordRequest request) {
authSignInService.resetPassword(request.email(), request.name());
return ResponseEntity.noContent().build();
}
}
60 changes: 60 additions & 0 deletions backend/src/main/java/sw_css/auth/api/SignUpController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package sw_css.auth.api;


import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import sw_css.auth.application.AuthEmailService;
import sw_css.auth.application.AuthSignUpService;
import sw_css.auth.application.dto.request.SendAuthCodeRequest;
import sw_css.auth.application.dto.request.SignUpRequest;
import sw_css.auth.application.dto.response.CheckDuplicateResponse;
import sw_css.auth.application.dto.response.SendAuthCodeResponse;

@Validated
@RequestMapping("/sign-up")
@RestController
@RequiredArgsConstructor
public class SignUpController {

private final AuthSignUpService authSignUpService;
private final AuthEmailService authEmailService;

@PostMapping
public ResponseEntity<Void> signUp(@RequestBody @Valid SignUpRequest request) {
long memberId = authSignUpService.signUp(request);
return ResponseEntity.created(URI.create("/members/" + memberId)).build();
}

@PostMapping("/send-auth-code")
public ResponseEntity<SendAuthCodeResponse> sendAuthCode(@RequestBody @Valid SendAuthCodeRequest request) {
return ResponseEntity.ok(authEmailService.emailAuth(request.email()));
}

@GetMapping("/exists/email")
public ResponseEntity<CheckDuplicateResponse> checkDuplicateEmail(
@RequestParam(value = "email") @NotBlank final String email) {
return ResponseEntity.ok(authSignUpService.isDuplicateEmail(email));
}

@GetMapping("/exists/student-id")
public ResponseEntity<CheckDuplicateResponse> checkDuplicateStudentId(
@RequestParam(value = "student_id") @NotBlank final String studentId) {
return ResponseEntity.ok(authSignUpService.isDuplicateStudentId(studentId));
}

@GetMapping("/exists/phone-number")
public ResponseEntity<CheckDuplicateResponse> checkDuplicatePhoneNumber(
@RequestParam(value = "phone_number") @NotBlank final String phoneNumber) {
return ResponseEntity.ok(authSignUpService.isDuplicatePhoneNumber(phoneNumber));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sw_css.auth.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sw_css.member.domain.repository.MemberRepository;
import sw_css.member.domain.repository.StudentMemberRepository;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthCheckDuplicateService {
private final MemberRepository memberRepository;
private final StudentMemberRepository studentMemberRepository;

public boolean isDuplicateEmail(String email) {
return memberRepository.existsByEmail(email);
}

public boolean isDuplicateStudentID(String studentIdStr) {
Long studentId = Long.parseLong(studentIdStr);
return studentMemberRepository.existsById(studentId);
}

public boolean isDuplicatePhoneNumber(String phoneNumber) {
return memberRepository.existsByPhoneNumber(phoneNumber);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sw_css.auth.application;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import sw_css.auth.application.dto.response.SendAuthCodeResponse;
import sw_css.auth.domain.EmailAuthRedis;
import sw_css.auth.domain.repository.EmailAuthRedisRepository;
import sw_css.utils.MailUtil;

@Service
@RequiredArgsConstructor
public class AuthEmailService {
public static final int EMAIL_EXPIRED_SECONDS = 600; // 10분
public static final int AUTH_CODE_LENGTH = 10;

private static final Random RANDOM = new Random();

private final MailUtil mailUtil;
private final EmailAuthRedisRepository emailAuthRedisRepository;

public SendAuthCodeResponse emailAuth(String email) {
String authCode = generateRandomAuthCode();
emailAuthRedisRepository.save(EmailAuthRedis.of(email, authCode));
sendAuthCode(email, authCode);
return SendAuthCodeResponse.from(EMAIL_EXPIRED_SECONDS);
}

public void sendNewPassword(String email, String password) {
List<String> toUserList = new ArrayList<>(List.of(email));
String subject = "[부산대학교] SW역량강화플랫폼 임시 비밀번호 발송 메일입니다.";
String text = "SW역량강화플랫폼 임시 비밀번호는 " + password + " 입니다.";
mailUtil.sendMail(toUserList, subject, text);
}

private static String generateRandomAuthCode() {
char leftLimit = '0';
char rightLimit = 'z';

return RANDOM.ints(leftLimit, rightLimit + 1)
.filter(i -> Character.isAlphabetic(i) || Character.isDigit(i))
.limit(AuthEmailService.AUTH_CODE_LENGTH)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}

private void sendAuthCode(String email, String authCode) {
List<String> toUserList = new ArrayList<>(List.of(email));
String subject = "[부산대학교] SW역량강화플랫폼 인증코드 발송 메일입니다.";
String text = "SW역량강화플랫폼 인증코드는 " + authCode + " 입니다.";
mailUtil.sendMail(toUserList, subject, text);
}
}
Loading

0 comments on commit 5974799

Please sign in to comment.