Skip to content

Commit

Permalink
feat: 행사 관리자 비밀번호 추가 (#213)
Browse files Browse the repository at this point in the history
* feat: 이벤트 비밀번호 추

* test: 테스트 공통 설정 클래스 분리

* feat: 어드민 인터셉터 추가 및 jwt 설정 추가

* feat: 이벤트 로그인 기능 구현

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

* feat: 쿠키 설정 분리

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

* submodule 업데이트

* style: 주석 제거

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

* refactor: 로컬 환경 쿠키 secure 옵션 제거

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

* test: 접근제어자 수정

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

* test: 개행 추가

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>

---------

Co-authored-by: 3juhwan <[email protected]>
Co-authored-by: kunsanglee <[email protected]>
Co-authored-by: khabh <[email protected]>
  • Loading branch information
4 people authored Aug 7, 2024
1 parent b2258ac commit 7c6f2c4
Show file tree
Hide file tree
Showing 40 changed files with 563 additions and 234 deletions.
8 changes: 7 additions & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

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

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
Expand Down
40 changes: 40 additions & 0 deletions server/src/main/java/server/haengdong/application/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package server.haengdong.application;


import java.util.Map;
import server.haengdong.domain.TokenProvider;
import server.haengdong.exception.AuthenticationException;

public class AuthService {

private static final String TOKEN_NAME = "eventToken";
private static final String CLAIM_SUB = "sub";

private final TokenProvider tokenProvider;

public AuthService(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}

public String createToken(String eventId) {
Map<String, Object> payload = Map.of(CLAIM_SUB, eventId);

return tokenProvider.createToken(payload);
}

public String findEventIdByToken(String token) {
validateToken(token);
Map<String, Object> payload = tokenProvider.getPayload(token);
return (String) payload.get(CLAIM_SUB);
}

private void validateToken(String token) {
if (!tokenProvider.validateToken(token)) {
throw new AuthenticationException();
}
}

public String getTokenName() {
return TOKEN_NAME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import server.haengdong.application.request.EventAppRequest;
import server.haengdong.application.request.EventLoginAppRequest;
import server.haengdong.application.request.MemberUpdateAppRequest;
import server.haengdong.application.response.ActionAppResponse;
import server.haengdong.application.response.EventAppResponse;
Expand All @@ -19,6 +20,7 @@
import server.haengdong.domain.event.Event;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.event.EventTokenProvider;
import server.haengdong.exception.AuthenticationException;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;

Expand All @@ -42,15 +44,13 @@ public EventAppResponse saveEvent(EventAppRequest request) {
}

public EventDetailAppResponse findEvent(String token) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT));
Event event = getEvent(token);

return EventDetailAppResponse.of(event);
}

public List<ActionAppResponse> findActions(String token) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT));
Event event = getEvent(token);

List<BillAction> billActions = billActionRepository.findByAction_Event(event).stream()
.sorted(Comparator.comparing(BillAction::getSequence)).toList();
Expand Down Expand Up @@ -92,8 +92,7 @@ private List<ActionAppResponse> getActionAppResponses(
}

public MembersAppResponse findAllMembers(String token) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT));
Event event = getEvent(token);

List<String> memberNames = memberActionRepository.findAllUniqueMemberByEvent(event);

Expand All @@ -102,8 +101,7 @@ public MembersAppResponse findAllMembers(String token) {

@Transactional
public void updateMember(String token, String memberName, MemberUpdateAppRequest request) {
Event event = eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT));
Event event = getEvent(token);
String updatedMemberName = request.name();
validateMemberNameUnique(event, updatedMemberName);

Expand All @@ -117,4 +115,16 @@ private void validateMemberNameUnique(Event event, String updatedMemberName) {
throw new HaengdongException(HaengdongErrorCode.DUPLICATED_MEMBER_NAME);
}
}

public void validatePassword(EventLoginAppRequest request) throws HaengdongException {
Event event = getEvent(request.token());
if (event.isSamePassword(request.password())) {
throw new AuthenticationException();
}
}

private Event getEvent(String token) {
return eventRepository.findByToken(token)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.NOT_FOUND_EVENT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import server.haengdong.domain.event.Event;

public record EventAppRequest(String name) {
public record EventAppRequest(String name, String password) {

public Event toEvent(String token) {
return new Event(name, token);
return new Event(name, password, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package server.haengdong.application.request;

public record EventLoginAppRequest(String token, String password) {
}
44 changes: 44 additions & 0 deletions server/src/main/java/server/haengdong/config/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package server.haengdong.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import server.haengdong.application.AuthService;
import server.haengdong.exception.AuthenticationException;
import server.haengdong.infrastructure.auth.AuthenticationExtractor;

@Slf4j
public class AdminInterceptor implements HandlerInterceptor {

private final AuthService authService;
private final AuthenticationExtractor authenticationExtractor;

public AdminInterceptor(AuthService authService, AuthenticationExtractor authenticationExtractor) {
this.authService = authService;
this.authenticationExtractor = authenticationExtractor;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.trace("login request = {}", request.getRequestURI());

String method = request.getMethod();
if (method.equals("GET")) {
return true;
}

validateToken(request);

return true;
}

private void validateToken(HttpServletRequest request) {
String token = authenticationExtractor.extract(request, authService.getTokenName());
String tokenEventId = authService.findEventIdByToken(token);
String eventId = request.getRequestURI().split("/")[2];
if (!tokenEventId.equals(eventId)) {
throw new AuthenticationException();
}
}
}
27 changes: 0 additions & 27 deletions server/src/main/java/server/haengdong/config/WebConfig.java

This file was deleted.

65 changes: 65 additions & 0 deletions server/src/main/java/server/haengdong/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package server.haengdong.config;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import server.haengdong.application.AuthService;
import server.haengdong.domain.TokenProvider;
import server.haengdong.infrastructure.auth.AuthenticationExtractor;
import server.haengdong.infrastructure.auth.JwtTokenProvider;
import server.haengdong.infrastructure.auth.TokenProperties;

@RequiredArgsConstructor
@EnableConfigurationProperties(TokenProperties.class)
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private final TokenProperties tokenProperties;

@Value("${cors.max-age}")
private Long maxAge;

@Value("${cors.allowed-origins}")
private String[] allowedOrigins;

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(maxAge);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/events");
}

@Bean
public AdminInterceptor adminInterceptor() {
return new AdminInterceptor(authService(), authenticationExtractor());
}

@Bean
public AuthService authService() {
return new AuthService(tokenProvider());
}

@Bean
public TokenProvider tokenProvider() {
return new JwtTokenProvider(tokenProperties);
}

@Bean
public AuthenticationExtractor authenticationExtractor() {
return new AuthenticationExtractor();
}
}
12 changes: 12 additions & 0 deletions server/src/main/java/server/haengdong/domain/TokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package server.haengdong.domain;

import java.util.Map;

public interface TokenProvider {

String createToken(Map<String, Object> payload);

Map<String, Object> getPayload(String token);

boolean validateToken(String token);
}
20 changes: 19 additions & 1 deletion server/src/main/java/server/haengdong/domain/event/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -18,18 +20,23 @@ public class Event {
private static final int MIN_NAME_LENGTH = 2;
private static final int MAX_NAME_LENGTH = 20;
private static final String SPACES = " ";
private static final Pattern PASSWORD_PATTERN = Pattern.compile("^\\d{4}$");

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private String password;

private String token;

public Event(String name, String token) {
public Event(String name, String password, String token) {
validateName(name);
validatePassword(password);
this.name = name;
this.password = password;
this.token = token;
}

Expand All @@ -48,11 +55,22 @@ private void validateName(String name) {
}
}

private void validatePassword(String password) {
Matcher matcher = PASSWORD_PATTERN.matcher(password);
if (!matcher.matches()) {
throw new HaengdongException(HaengdongErrorCode.BAD_REQUEST, "비밀번호는 4자리 숫자만 가능합니다.");
}
}

private boolean isBlankContinuous(String name) {
return name.contains(SPACES);
}

public boolean isTokenMismatch(String token) {
return !this.token.equals(token);
}

public boolean isSamePassword(String password) {
return this.password.equals(password);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package server.haengdong.exception;

public class AuthenticationException extends RuntimeException {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
Expand All @@ -14,6 +15,12 @@
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> authenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ErrorResponse.of(HaengdongErrorCode.UNAUTHORIZED));
}

@ExceptionHandler({HttpRequestMethodNotSupportedException.class, NoResourceFoundException.class})
public ResponseEntity<ErrorResponse> noResourceException() {
return ResponseEntity.badRequest()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum HaengdongErrorCode {
BAD_REQUEST("R_001", "잘못된 요청입니다."),
NO_RESOURCE_REQUEST("R_002", "잘못된 엔드포인트입니다."),
MESSAGE_NOT_READABLE("R_003", "읽을 수 없는 요청 형식입니다."),
UNAUTHORIZED("A_001", "인증에 실패했습니다."),
INTERNAL_SERVER_ERROR("S_001", "서버 내부에서 에러가 발생했습니다."),
DUPLICATED_MEMBER_NAME("EV_001", "중복된 행사 참여 인원 이름이 존재합니다."),
NOT_FOUND_EVENT("EV_400", "존재하지 않는 행사입니다."),
Expand Down
Loading

0 comments on commit 7c6f2c4

Please sign in to comment.