Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 스태프 권한 인증 기능 구현 (#478) #480

Draft
wants to merge 30 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cbad3bf
feat: School domain 검증 추가
xxeol2 Sep 28, 2023
fe45912
feat: School 약어 조회 기능 구현
xxeol2 Sep 28, 2023
803bd40
feat: StaffCode 엔티티 정의
xxeol2 Sep 28, 2023
9dc30a8
feat: StaffVerificationCodeProvider 정의
xxeol2 Sep 28, 2023
1f8c53e
feat: StaffCode 생성 기능 구현
xxeol2 Sep 28, 2023
4a3d57c
fix: staffCodeRepository.existsByCode parameter type 수정
xxeol2 Sep 28, 2023
be1d5f5
feat: 축제 생성시 스태프 코드 생성 로직 구현
xxeol2 Sep 28, 2023
182dfd9
feat: StaffVerificationCode not null 제약조건 추가
xxeol2 Sep 28, 2023
6dd58bc
refactor: Transactional import 수정
xxeol2 Sep 28, 2023
d013bd6
refactor: 클래스 네이밍 수정 AdminAuthFacadeServiceTest -> AdminAuthServiceTest
xxeol2 Sep 28, 2023
5860245
feat: staff 로그인 기능 구현
xxeol2 Sep 28, 2023
060abff
test: FestivalServiceTest에 ApplicationEventPublisher 주입
xxeol2 Sep 28, 2023
54fd7c7
feat: 스태프 로그인 API 구현
xxeol2 Sep 28, 2023
b7f7431
refactor: StaffController 통합
xxeol2 Sep 28, 2023
dd4f1aa
refactor: sout문 삭제
xxeol2 Sep 28, 2023
1bd85fa
feat: Staff 인가 추가
xxeol2 Sep 28, 2023
3edd22d
refactor: StaffCode - Festival OneToOne으로 변경
xxeol2 Sep 28, 2023
bb03de0
refactor: StaffCode 값객체가 아닌 원시값 사용하도록 수정
xxeol2 Sep 28, 2023
3fb94d4
Revert "refactor: StaffCode 값객체가 아닌 원시값 사용하도록 수정"
xxeol2 Oct 1, 2023
84d7206
refactor: 도메인 네이밍 수정
xxeol2 Oct 1, 2023
01fb56a
style: 개행 추가
xxeol2 Oct 1, 2023
6fba6ab
refactor: Validation 추가
xxeol2 Oct 1, 2023
4d83bbc
refactor: 불필요한 static 키워드 제거
xxeol2 Oct 1, 2023
f4d7a16
style: 불필요한 개행 제거
xxeol2 Oct 1, 2023
b85aa9e
refactor: ErrorCode 오타 수정
xxeol2 Oct 1, 2023
7e8a268
refactor: 스태프 로그인시 staffId 전달하도록 수정
xxeol2 Oct 1, 2023
c3c1d25
Merge remote-tracking branch 'origin/dev' into feat/#478
xxeol2 Oct 5, 2023
a79cec0
refactor: lombok 적용
xxeol2 Oct 6, 2023
d850d2c
wip
xxeol2 Oct 6, 2023
c6c6e8c
Merge remote-tracking branch 'origin/dev' into feat/#478
xxeol2 Oct 6, 2023
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
12 changes: 12 additions & 0 deletions backend/src/main/java/com/festago/auth/annotation/Staff.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.festago.auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Staff {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.festago.auth.application;

import com.festago.auth.domain.AuthPayload;
import com.festago.auth.domain.Role;
import com.festago.auth.dto.StaffLoginRequest;
import com.festago.auth.dto.StaffLoginResponse;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.UnauthorizedException;
import com.festago.festival.domain.Festival;
import com.festago.staff.domain.StaffCode;
import com.festago.staff.repository.StaffCodeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class StaffAuthService {

private final AuthProvider authProvider;
private final StaffCodeRepository staffCodeRepository;

public StaffAuthService(AuthProvider authProvider, StaffCodeRepository staffCodeRepository) {
this.authProvider = authProvider;
this.staffCodeRepository = staffCodeRepository;
}

@Transactional(readOnly = true)
public StaffLoginResponse login(StaffLoginRequest request) {
StaffCode staffCode = findStaffCode(request.code());
Festival festival = staffCode.getFestival();
String accessToken = authProvider.provide(createAuthPayload(festival));
return new StaffLoginResponse(festival.getId(), accessToken);
}

private StaffCode findStaffCode(String code) {
return staffCodeRepository.findByCodeWithFetch(code)
.orElseThrow(() -> new UnauthorizedException(ErrorCode.INCORRECT_STAFF_CODE));
}

private AuthPayload createAuthPayload(Festival festival) {
return new AuthPayload(festival.getId(), Role.STAFF);
}
}
14 changes: 14 additions & 0 deletions backend/src/main/java/com/festago/auth/config/LoginConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public LoginConfig(AuthExtractor authExtractor, AuthenticateContext context) {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new RoleArgumentResolver(Role.MEMBER, authenticateContext));
resolvers.add(new RoleArgumentResolver(Role.ADMIN, authenticateContext));
resolvers.add(new RoleArgumentResolver(Role.STAFF, authenticateContext));
}

@Override
Expand All @@ -39,6 +40,9 @@ public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(memberAuthInterceptor())
.addPathPatterns("/member-tickets/**", "/members/**", "/auth/**", "/students/**")
.excludePathPatterns("/auth/oauth2");
registry.addInterceptor(staffAuthInterceptor())
.addPathPatterns("/staff/**")
.excludePathPatterns("/staff/login");
}

@Bean
Expand All @@ -60,4 +64,14 @@ public AuthInterceptor memberAuthInterceptor() {
.role(Role.MEMBER)
.build();
}

@Bean
public AuthInterceptor staffAuthInterceptor() {
return AuthInterceptor.builder()
.authExtractor(authExtractor)
.tokenExtractor(new HeaderTokenExtractor())
.authenticateContext(authenticateContext)
.role(Role.STAFF)
.build();
}
}
3 changes: 2 additions & 1 deletion backend/src/main/java/com/festago/auth/domain/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.festago.auth.annotation.Admin;
import com.festago.auth.annotation.Anonymous;
import com.festago.auth.annotation.Member;
import com.festago.auth.annotation.Staff;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import java.lang.annotation.Annotation;
Expand All @@ -11,7 +12,7 @@ public enum Role {
ANONYMOUS(Anonymous.class),
MEMBER(Member.class),
ADMIN(Admin.class),
;
STAFF(Staff.class);
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

private final Class<? extends Annotation> annotation;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.festago.auth.dto;

public record StaffLoginRequest(
String code
) {

}
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.auth.dto;

public record StaffLoginResponse(
Long festivalId,
String accessToken
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public enum ErrorCode {
DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."),
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),
DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."),
STAFF_CODE_EXIST("이미 스태프 코드가 존재합니다."),
INVALID_SCHOOL_DOMAIN("올바르지 않은 학교 도에인입니다."),
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand All @@ -33,7 +36,7 @@ public enum ErrorCode {
NEED_AUTH_TOKEN("로그인이 필요한 서비스입니다."),
INCORRECT_PASSWORD_OR_ACCOUNT("비밀번호가 틀렸거나, 해당 계정이 없습니다."),
DUPLICATE_ACCOUNT_USERNAME("해당 계정이 존재합니다."),
DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."),
INCORRECT_STAFF_CODE("올바르지 않은 스태프 코드입니다."),

// 403
NOT_ENOUGH_PERMISSION("해당 권한이 없습니다."),
Expand All @@ -59,7 +62,8 @@ public enum ErrorCode {
INVALID_ROLE_NAME("해당하는 Role이 없습니다."),
FOR_TEST_ERROR("테스트용 에러입니다."),
FAIL_SEND_FCM_MESSAGE("FCM Message 전송에 실패했습니다."),
FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다.");
FCM_NOT_FOUND("유효하지 않은 MemberFCM 이 감지 되었습니다."),
;

private final String message;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.ForbiddenException;
import com.festago.common.exception.NotFoundException;
import com.festago.entry.domain.EntryCode;
import com.festago.entry.domain.EntryCodePayload;
Expand Down Expand Up @@ -51,11 +52,18 @@ private MemberTicket findMemberTicket(Long memberTicketId) {
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_TICKET_NOT_FOUND));
}

public TicketValidationResponse validate(TicketValidationRequest request) {
public TicketValidationResponse validate(TicketValidationRequest request, Long festivalId) {
EntryCodePayload entryCodePayload = entryCodeManager.extract(request.code());
MemberTicket memberTicket = findMemberTicket(entryCodePayload.getMemberTicketId());
checkPermission(festivalId, memberTicket);
memberTicket.changeState(entryCodePayload.getEntryState());
publisher.publishEvent(new EntryProcessEvent(memberTicket.getOwner().getId()));
return TicketValidationResponse.from(memberTicket);
}

private static void checkPermission(Long festivalId, MemberTicket memberTicket) {
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved
if (!memberTicket.belongsFestival(festivalId)) {
throw new ForbiddenException(ErrorCode.NOT_ENOUGH_PERMISSION);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
import com.festago.festival.dto.FestivalDetailResponse;
import com.festago.festival.dto.FestivalResponse;
import com.festago.festival.dto.FestivalsResponse;
import com.festago.festival.dto.event.FestivalCreateEvent;
import com.festago.festival.repository.FestivalRepository;
import com.festago.school.domain.School;
import com.festago.school.repository.SchoolRepository;
import com.festago.stage.domain.Stage;
import com.festago.stage.repository.StageRepository;
import java.time.LocalDate;
import java.util.List;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -27,12 +29,15 @@ public class FestivalService {
private final FestivalRepository festivalRepository;
private final StageRepository stageRepository;
private final SchoolRepository schoolRepository;
private final ApplicationEventPublisher publisher;

xxeol2 marked this conversation as resolved.
Show resolved Hide resolved

public FestivalService(FestivalRepository festivalRepository, StageRepository stageRepository,
SchoolRepository schoolRepository) {
SchoolRepository schoolRepository, ApplicationEventPublisher publisher) {
this.festivalRepository = festivalRepository;
this.stageRepository = stageRepository;
this.schoolRepository = schoolRepository;
this.publisher = publisher;
}

public FestivalResponse create(FestivalCreateRequest request) {
Expand All @@ -41,6 +46,7 @@ public FestivalResponse create(FestivalCreateRequest request) {
Festival festival = request.toEntity(school);
validate(festival);
Festival newFestival = festivalRepository.save(festival);
publisher.publishEvent(new FestivalCreateEvent(newFestival.getId()));
return FestivalResponse.from(newFestival);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.festago.festival.dto.event;

public record FestivalCreateEvent(
Long festivalId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.festago.presentation;

import com.festago.auth.annotation.Staff;
import com.festago.auth.application.StaffAuthService;
import com.festago.auth.dto.StaffLoginRequest;
import com.festago.auth.dto.StaffLoginResponse;
import com.festago.entry.application.EntryService;
import com.festago.entry.dto.TicketValidationRequest;
import com.festago.entry.dto.TicketValidationResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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("/staff")
@Tag(name = "스태프 요청")
public class StaffController {

private final StaffAuthService staffAuthService;
private final EntryService entryService;


public StaffController(StaffAuthService staffAuthService, EntryService entryService) {
this.staffAuthService = staffAuthService;
this.entryService = entryService;
}

@PostMapping("/login")
@Operation(description = "스태프 코드로 로그인한다.", summary = "스태프 로그인")
public ResponseEntity<StaffLoginResponse> login(@RequestBody StaffLoginRequest request) {
xxeol2 marked this conversation as resolved.
Show resolved Hide resolved
StaffLoginResponse response = staffAuthService.login(request);
return ResponseEntity.ok()
.body(response);
}

@PostMapping("/member-tickets/validation")
@Operation(description = "스태프가 티켓을 검사한다.", summary = "티켓 검사")
public ResponseEntity<TicketValidationResponse> validate(
@RequestBody @Valid TicketValidationRequest request,
@Staff Long festivalId) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Staff Long festivalId) {
@Staff Long staffId) {

또한 entryService의 validate 메서드 또한, 파라미터 명이 잘못된 것 같네요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드로 인증할 때, jwt 토큰에 StaffCode 정보를 담지 않고 Festival 정보를 담아서 festivalId를 의도한게 맞긴합니다..!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staff정보를 담는게 더 좋을까요 ?!
그렇게 하면 Staff -> Festival 한 단계 타고 가야해서 이렇게 설계했었어요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금보니 Staff 도메인의 Staff 엔티티가 StaffCode로 되어 있군요 😂
Staff로 이름을 바꾸는게 더 명확해보이네요.
지금 같은 방법을 사용하면 티켓을 검증할 때 바로 festivalId로 MemberTicket 엔티티에서 belongsFestival 메서드를 호출하여 추가적인 쿼리문 없이 티켓을 검증할 수 있겠네요!
하지만 코드 상에서 흐름을 읽기가 더 어려워졌지 않나 싶습니다.. (컨트롤러에서 @Staff로 받아온 Long 타입은 staff의 Id라고 당연하게 생각할 수 있으므로)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7e8a268 staffId를 넘기는 방식으로 반영완료했습니다!

TicketValidationResponse response = entryService.validate(request, festivalId);
return ResponseEntity.ok()
.body(response);
}
}

This file was deleted.

17 changes: 17 additions & 0 deletions backend/src/main/java/com/festago/school/domain/School.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.festago.school.domain;

import com.festago.common.domain.BaseTimeEntity;
import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import jakarta.persistence.Column;
Expand All @@ -10,10 +11,14 @@
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.regex.Pattern;

@Entity
public class School extends BaseTimeEntity {

private static final Pattern DOMAIN_REGEX = Pattern.compile("^[^.]+(\\.[^.]+)+$");
private static final char DOMAIN_DELIMITER = '.';

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand Down Expand Up @@ -45,6 +50,7 @@ public School(Long id, String domain, String name) {
private void validate(String domain, String name) {
checkNotNull(domain, name);
checkLength(domain, name);
validateDomain(domain);
}

private void checkNotNull(String domain, String name) {
Expand All @@ -68,6 +74,17 @@ private boolean overLength(String target, int maxLength) {
return target.length() > maxLength;
}

private void validateDomain(String domain) {
if (!DOMAIN_REGEX.matcher(domain).matches()) {
throw new BadRequestException(ErrorCode.INVALID_SCHOOL_DOMAIN);
}
}

public String findAbbreviation() {
int dotIndex = domain.indexOf(DOMAIN_DELIMITER);
return domain.substring(0, dotIndex);
}

public Long getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.festago.staff.application;

import static java.util.stream.Collectors.joining;

import com.festago.festival.domain.Festival;
import com.festago.staff.domain.StaffCode;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.stereotype.Component;

@Component
public class RandomStaffCodeProvider implements StaffCodeProvider {
Comment on lines +1 to +12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RandomStaffCodeProvider의 패키지 위치

RandomStaffCodeProvider 클래스를 infrastructure가 아닌 application 패키지에 위치시켰습니다.

infrastructure 패키지는 “외부 종속성 분리”를 위한 패키지인데, RandomStaffCodePRovider에는 외부 종속성이 전혀 없고 비즈니스 로직을 담았기 때문에 application 계층이 적합하다 판단했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 애쉬 생각에 동의해요. Random 값을 뿌려주기에 interface를 구현 하는게 좋을 것 같고, 외부 의존성 없기 때문에 같은 패키지로 가도 문제없다고 생각해요.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동의보감입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비즈니스 로직을 담고 있다면 domain 계층이 더 올바르다고 생각하는데 이 의견은 어떠신가요?

Copy link
Collaborator

@seokjin8678 seokjin8678 Oct 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

덤으로 student 패키지의 RandomVerificationCodeProvider의 위치 또한 고민해봤으면 좋겠네요


@Override
public String provide(Festival festival) {
String abbreviation = festival.getSchool().findAbbreviation();
Random random = ThreadLocalRandom.current();
String code = random.ints(0, 10)
.mapToObj(String::valueOf)
.limit(StaffCode.RANDOM_CODE_LENGTH)
.collect(joining());
return abbreviation + code;
}
}
Loading