Skip to content

Commit

Permalink
Merge pull request #10 from woowacourse-integrated-service/feature/#9
Browse files Browse the repository at this point in the history
feat: 회원가입 기능 추가
  • Loading branch information
parkmuhyeun authored Sep 23, 2023
2 parents 1510230 + cf9f242 commit 2e495a0
Show file tree
Hide file tree
Showing 46 changed files with 1,056 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
.idea/codeStyles
.idea

src/main/resources/jwt-env.properties
src/main/resources/database-env.properties
Expand Down
20 changes: 16 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,29 @@ 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.mindrot:jbcrypt:0.4'

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation("it.ozimov:embedded-redis:0.7.2")

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

// Mail
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// acceptance test
testImplementation 'io.rest-assured:rest-assured:5.3.0'
testImplementation 'io.rest-assured:rest-assured:5.3.1'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Expand Down Expand Up @@ -89,7 +98,9 @@ jacocoTestReport {
'**/*ExceptionHandler*',
'**/LoggingUtils',
'**/*Url*',
'**/*AdminController*'
'**/*AdminController*',
'**/*Config*',
'**/*Wrapper*'
])
})
)
Expand All @@ -113,7 +124,7 @@ jacocoTestCoverageVerification {
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.9
minimum = 0.7
}

excludes = [
Expand All @@ -130,7 +141,8 @@ jacocoTestCoverageVerification {
'*.LoggingUtils',
'*.*Url',
'*.*AdminController',
"*.*QuerydslConfig"
'*.*Config',
'*.*Wrapper'
] + Qdomains
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.integrated.techhub.auth.application;

import com.integrated.techhub.member.domain.repository.MemberRepository;
import com.integrated.techhub.member.exception.MemberExistsException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthQueryService {

private final MemberRepository memberRepository;

public void validateExistedMember(final String email) {
if (memberRepository.existsByEmail(email)) {
throw new MemberExistsException();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.integrated.techhub.auth.application;

import com.integrated.techhub.auth.domain.PasswordEncoder;
import com.integrated.techhub.auth.dto.SignUpRequest;
import com.integrated.techhub.member.domain.Member;
import com.integrated.techhub.member.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class AuthService {

private final MemberRepository memberRepository;
private final AuthQueryService authQueryService;
private final PasswordEncoder passwordEncoder;

public Long registerMember(final SignUpRequest request) {
authQueryService.validateExistedMember(request.email());
Member member = request.toEntity();
member.encodePassword(passwordEncoder);
return memberRepository.save(member).getId();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.integrated.techhub.auth.domain;

public interface PasswordEncoder {

String encode(String rawPassword);

boolean isMatch(String rawPassword, String encryptedPassword);

}
48 changes: 48 additions & 0 deletions src/main/java/com/integrated/techhub/auth/dto/SignUpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.integrated.techhub.auth.dto;

import com.integrated.techhub.member.domain.Member;
import com.integrated.techhub.member.domain.Position;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

public record SignUpRequest(
@Email(message = "이메일 형식이어야 합니다. 올바른 형식인지 확인해주세요.")
@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String email,

@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).{8,}$", message = "최소 8자, 문자 1개와 숫자 1개 이상을 포함해야합니다. 다시 입력해주세요.")
@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String password,

@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String nickname,

@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String githubUserName,

@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String name,

@Pattern(regexp = "^(BE|FE|AOS)$", message = "BE, FE, AOS만 입력할 수 있습니다. 알맞은 포지션인지 확인해주세요.")
@NotBlank(message = "Null 또는 공백이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
String position,

@NotNull(message = "Null이 포함될 수 없습니다. 올바른 값인지 확인해주세요.")
Integer cardinalNumber
) {

public Member toEntity() {
return Member.builder()
.email(email)
.password(password)
.nickname(nickname)
.githubUsername(githubUserName)
.name(name)
.position(Position.valueOf(position))
.cardinalNumber(cardinalNumber)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.integrated.techhub.auth.infra.encode;

import com.integrated.techhub.auth.domain.PasswordEncoder;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Component;

@Component
public class BCryptPasswordEncoder implements PasswordEncoder {

@Override
public String encode(String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt());
}

@Override
public boolean isMatch(String rawPassword, String encryptedPassword) {
return BCrypt.checkpw(rawPassword, encryptedPassword);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.integrated.techhub.auth.presentation;

import com.integrated.techhub.auth.application.AuthService;
import com.integrated.techhub.auth.dto.SignUpRequest;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

private final AuthService authService;

@PostMapping("/sign-up")
public ResponseEntity<Void> signUp(@RequestBody @Valid final SignUpRequest request) {
final Long memberId = authService.registerMember(request);
return ResponseEntity.created(URI.create("/members/" + memberId)).build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.integrated.techhub.common.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Slf4j
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

@Override
@Bean(name = "MailExecutor")
public Executor getAsyncExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("MailExecutor-");
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Exception handler for async method '" + method.toGenericString()
+ "' threw unexpected exception itself", ex);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.integrated.techhub.common.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import redis.embedded.RedisServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@Slf4j
@Configuration
public class EmbeddedRedisConfig {

@Value("${spring.data.redis.port}")
private int redisPort;

private RedisServer redisServer;

@PostConstruct
public void redisServer() throws IOException {
int port = isRedisRunning()? findAvailablePort() : redisPort;
redisServer = new RedisServer(port);
redisServer.start();
}

/**
* Embedded Redis가 현재 실행중인지 확인
*/
private boolean isRedisRunning() throws IOException {
return isRunning(executeGrepProcessCommand(redisPort));
}

/**
* 해당 port를 사용중인 프로세스 확인하는 sh 실행
*/
private Process executeGrepProcessCommand(int port) throws IOException {
String command = String.format("netstat -nat | grep LISTEN|grep %d", port);
String[] shell = {"/bin/sh", "-c", command};
return Runtime.getRuntime().exec(shell);
}

/**
* 해당 Process가 현재 실행중인지 확인
*/
private boolean isRunning(Process process) {
String line;
StringBuilder pidInfo = new StringBuilder();

try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {

while ((line = input.readLine()) != null) {
pidInfo.append(line);
}

} catch (Exception e) {
}

return !StringUtils.isEmpty(pidInfo.toString());
}

/**
* 현재 PC/서버에서 사용가능한 포트 조회
*/
public int findAvailablePort() throws IOException {

for (int port = 10000; port <= 65535; port++) {
Process process = executeGrepProcessCommand(port);
if (!isRunning(process)) {
return port;
}
}

throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535");
}

@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.integrated.techhub.common.exception;

import org.springframework.http.HttpStatus;

public record ErrorCode(
HttpStatus status,
String message
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.integrated.techhub.common.exception;

public record ErrorResponse(int status, String message) {

@Override
public String toString() {
return "{\n" +
"\t\"status\": " + status +
",\n\t\"message\": \"" + message + '\"' +
"\n}";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.integrated.techhub.common.exception;

import lombok.Getter;

@Getter
public class TechHubException extends RuntimeException {

private final ErrorCode errorCode;

public TechHubException(ErrorCode errorCode) {
super(errorCode.message());
this.errorCode = errorCode;
}

}
Loading

0 comments on commit 2e495a0

Please sign in to comment.