feat: 일반 회원가입
This commit is contained in:
13
src/main/java/com/audio/Application.java
Normal file
13
src/main/java/com/audio/Application.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.audio;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
7
src/main/java/com/audio/common/ApiResponse.java
Normal file
7
src/main/java/com/audio/common/ApiResponse.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.audio.common;
|
||||
|
||||
public record ApiResponse<T>(T data) {
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(data);
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/audio/common/GlobalExceptionResponse.java
Normal file
18
src/main/java/com/audio/common/GlobalExceptionResponse.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.audio.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class GlobalExceptionResponse {
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private String status;
|
||||
private String instance;
|
||||
}
|
||||
16
src/main/java/com/audio/common/HealthCheckController.java
Normal file
16
src/main/java/com/audio/common/HealthCheckController.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.audio.common;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/health-check")
|
||||
public class HealthCheckController {
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Boolean> healthCheck() {
|
||||
return ResponseEntity.ok(true);
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/audio/config/GlobalApiResponseAdvice.java
Normal file
31
src/main/java/com/audio/config/GlobalApiResponseAdvice.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.audio.config;
|
||||
|
||||
import com.audio.common.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class GlobalApiResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return returnType.getParameterType().equals(ResponseEntity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
Object body, MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request, ServerHttpResponse response) {
|
||||
|
||||
return ApiResponse.ok(body);
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/audio/config/GlobalExceptionHandler.java
Normal file
77
src/main/java/com/audio/config/GlobalExceptionHandler.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.audio.config;
|
||||
|
||||
import com.audio.common.GlobalExceptionResponse;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler
|
||||
implements AccessDeniedHandler, AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleErrorResponseException(
|
||||
ErrorResponseException ex, HttpHeaders headers,
|
||||
HttpStatusCode status, WebRequest request
|
||||
) {
|
||||
HttpStatus httpStatus = HttpStatus.valueOf(ex.getStatusCode().value());
|
||||
|
||||
String message = ex.getBody().getDetail() != null
|
||||
? ex.getBody().getDetail()
|
||||
: ex.getMessage();
|
||||
|
||||
String uri = null;
|
||||
if (request instanceof ServletWebRequest servletWebRequest) {
|
||||
HttpServletRequest httpReq = servletWebRequest.getRequest();
|
||||
String queryString = httpReq.getQueryString();
|
||||
|
||||
uri = httpReq.getRequestURI();
|
||||
if (queryString != null) {
|
||||
uri += "?" + queryString;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalExceptionResponse body = GlobalExceptionResponse.builder()
|
||||
.code(httpStatus.value())
|
||||
.message(message)
|
||||
.status(httpStatus.name())
|
||||
.instance(uri)
|
||||
.build();
|
||||
|
||||
return ResponseEntity.status(httpStatus).body(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException
|
||||
) throws IOException, ServletException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(
|
||||
HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException
|
||||
) throws IOException, ServletException {
|
||||
|
||||
}
|
||||
}
|
||||
53
src/main/java/com/audio/config/SecurityConfig.java
Normal file
53
src/main/java/com/audio/config/SecurityConfig.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.audio.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
String[] allAuthorizedUrls = {
|
||||
"/health-check",
|
||||
"/api/v1/auth/**"
|
||||
};
|
||||
|
||||
http.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 허용 API
|
||||
.requestMatchers(allAuthorizedUrls).permitAll()
|
||||
// 그 외는 인증 필요
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
|
||||
// 이후 ex 핸들러로 변경
|
||||
.exceptionHandling(ex -> ex.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.getWriter().write("{\"message\":\"Forbidden\"}");
|
||||
}));
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.audio.core.auth.api.controller;
|
||||
|
||||
import com.audio.core.auth.api.dto.req.SignUpRequest;
|
||||
import com.audio.core.auth.api.dto.res.SignUpResponse;
|
||||
import com.audio.core.auth.application.RegisterUseCase;
|
||||
import com.audio.core.auth.application.dto.command.RegisterUseCaseCommand;
|
||||
import com.audio.core.auth.application.dto.result.RegisterUseCaseResult;
|
||||
import com.audio.core.auth.domain.service.AccountService;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AccountService accountService;
|
||||
private final RegisterUseCase registerUseCase;
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<SignUpResponse> register(
|
||||
@RequestBody SignUpRequest request
|
||||
) {
|
||||
RegisterUseCaseResult result = registerUseCase.execute(
|
||||
RegisterUseCaseCommand.builder()
|
||||
.loginId(request.getLoginId())
|
||||
.password(request.getPassword())
|
||||
.build()
|
||||
);
|
||||
|
||||
SignUpResponse response = SignUpResponse.builder()
|
||||
.accessToken(result.getAccessToken())
|
||||
.refreshToken(result.getRefreshToken())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.audio.core.auth.api.dto.req;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class SignUpRequest {
|
||||
|
||||
private String loginId;
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.audio.core.auth.api.dto.res;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SignUpResponse {
|
||||
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.audio.core.auth.application;
|
||||
|
||||
import com.audio.core.auth.application.dto.command.RegisterUseCaseCommand;
|
||||
import com.audio.core.auth.application.dto.result.RegisterUseCaseResult;
|
||||
import com.audio.core.auth.domain.service.AccountService;
|
||||
import com.audio.core.exception.auth.ExistLoginIdException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public final class RegisterUseCase {
|
||||
|
||||
private final AccountService accountService;
|
||||
|
||||
public RegisterUseCaseResult execute(RegisterUseCaseCommand command) {
|
||||
String loginId = command.getLoginId();
|
||||
|
||||
boolean isExistLoginId = accountService.isExistLoginId(loginId);
|
||||
|
||||
if (isExistLoginId) {
|
||||
throw new ExistLoginIdException(loginId);
|
||||
}
|
||||
|
||||
long accountId = accountService.register(loginId, command.getPassword());
|
||||
|
||||
return RegisterUseCaseResult.builder()
|
||||
.accessToken("accessToken")
|
||||
.refreshToken("refreshToken")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.audio.core.auth.application.dto.command;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class RegisterUseCaseCommand {
|
||||
|
||||
private String loginId;
|
||||
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.audio.core.auth.application.dto.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class RegisterUseCaseResult {
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private String refreshToken;
|
||||
}
|
||||
41
src/main/java/com/audio/core/auth/domain/entity/Account.java
Normal file
41
src/main/java/com/audio/core/auth/domain/entity/Account.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.audio.core.auth.domain.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
|
||||
@Entity
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@Getter
|
||||
@ToString
|
||||
public class Account {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String loginId;
|
||||
private String passwordHash;
|
||||
private String name;
|
||||
|
||||
@Builder.Default
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@Builder.Default
|
||||
@LastModifiedDate
|
||||
private LocalDateTime updatedAt = LocalDateTime.now();
|
||||
|
||||
@Builder.Default
|
||||
private Boolean deleted = false;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.audio.core.auth.domain.repository;
|
||||
|
||||
public interface AccountQueryRepository {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.audio.core.auth.domain.repository;
|
||||
|
||||
import com.audio.core.auth.domain.entity.Account;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||
Optional<Account> findByLoginId(String loginId);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.audio.core.auth.domain.service;
|
||||
|
||||
import com.audio.core.auth.domain.entity.Account;
|
||||
import com.audio.core.auth.domain.repository.AccountRepository;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AccountService {
|
||||
|
||||
private final AccountRepository accountRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Transactional
|
||||
public long register(String loginId, String password) {
|
||||
log.info("회원 등록: {}", loginId);
|
||||
|
||||
String hashedPassword = passwordEncoder.encode(password);
|
||||
|
||||
Account account = Account.builder()
|
||||
.loginId(loginId)
|
||||
.name("임시 이름")
|
||||
.passwordHash(hashedPassword)
|
||||
.build();
|
||||
|
||||
log.info("account: {}", account);
|
||||
|
||||
accountRepository.save(account);
|
||||
|
||||
return account.getId();
|
||||
}
|
||||
|
||||
public boolean isExistLoginId(String loginId) {
|
||||
log.info("로그인 아이디 존재 여부 조회");
|
||||
|
||||
return accountRepository.findByLoginId(loginId).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.audio.core.auth.infra.repository;
|
||||
|
||||
import com.audio.core.auth.domain.repository.AccountQueryRepository;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class AccountQueryDslRepository implements AccountQueryRepository {
|
||||
|
||||
private final JPAQueryFactory queryFactory;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.audio.core.exception.auth;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.web.ErrorResponseException;
|
||||
|
||||
public class ExistLoginIdException extends ErrorResponseException {
|
||||
|
||||
public ExistLoginIdException(String loginId) {
|
||||
super(
|
||||
HttpStatus.CONFLICT,
|
||||
ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.CONFLICT,
|
||||
"이미 존재하는 loginId 입니다: " + loginId
|
||||
),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
3
src/main/resources/application.yaml
Normal file
3
src/main/resources/application.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
spring:
|
||||
application:
|
||||
name: audio_book
|
||||
21
src/main/resources/config/application.yaml
Normal file
21
src/main/resources/config/application.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
application:
|
||||
name: audio_book
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
# dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
liquibase:
|
||||
change-log: classpath:/db/master.yaml
|
||||
enabled: true
|
||||
|
||||
#logging:
|
||||
# level:
|
||||
# org.hibernate.SQL: null
|
||||
# org.hibernate.type: trace
|
||||
3
src/main/resources/db/changelog/changelog.yaml
Normal file
3
src/main/resources/db/changelog/changelog.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
databaseChangeLog:
|
||||
- include:
|
||||
file: db/sql/account/251203/1_create_account.sql
|
||||
3
src/main/resources/db/master.yaml
Normal file
3
src/main/resources/db/master.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
databaseChangeLog:
|
||||
- include:
|
||||
file: db/changelog/changelog.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
-- liquibase formatted sql
|
||||
-- changeset corpi:1_create_account
|
||||
CREATE TABLE account
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
login_id VARCHAR(255) NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
13
src/test/java/com/audio/ApplicationTests.java
Normal file
13
src/test/java/com/audio/ApplicationTests.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.audio;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class ApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user