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 all 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
4 changes: 2 additions & 2 deletions backend/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.8'
id 'org.springframework.boot' version '3.1.4'
id 'io.spring.dependency-management' version '1.1.0'
}

Expand Down Expand Up @@ -42,7 +42,7 @@ dependencies {
implementation "com.github.maricn:logback-slack-appender:1.4.0"

// Mockito
testImplementation 'org.mockito:mockito-inline'
testImplementation 'org.mockito:mockito-inline:5.2.0'

// Cucumber
testImplementation 'io.cucumber:cucumber-java:7.13.0'
Expand Down
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,38 @@
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.staff.domain.Staff;
import com.festago.staff.repository.StaffRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class StaffAuthService {

private final AuthProvider authProvider;
private final StaffRepository staffRepository;

@Transactional(readOnly = true)
public StaffLoginResponse login(StaffLoginRequest request) {
Staff staff = findStaffCode(request.code());
String accessToken = authProvider.provide(createAuthPayload(staff));
return new StaffLoginResponse(staff.getId(), accessToken);
}

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

private AuthPayload createAuthPayload(Staff staff) {
return new AuthPayload(staff.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 @@ -26,6 +26,7 @@ public class LoginConfig implements WebMvcConfigurer {
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 @@ -36,6 +37,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 @@ -57,4 +61,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();
}
}
2 changes: 2 additions & 0 deletions 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,6 +12,7 @@ public enum Role {
ANONYMOUS(Anonymous.class),
MEMBER(Member.class),
ADMIN(Admin.class),
STAFF(Staff.class),
;

private final Class<? extends Annotation> annotation;
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main/java/com/festago/auth/dto/StaffLoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.festago.auth.dto;

import jakarta.validation.constraints.NotBlank;

public record StaffLoginRequest(
@NotBlank(message = "code는 공백일 수 없습니다.")
String code
) {

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

public record StaffLoginResponse(
Long staffId,
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("올바르지 않은 학교 도메인입니다."),


// 401
Expand All @@ -34,7 +37,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 @@ -46,6 +49,7 @@ public enum ErrorCode {
FESTIVAL_NOT_FOUND("존재하지 않는 축제입니다."),
TICKET_NOT_FOUND("존재하지 않는 티켓입니다."),
SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."),
STAFF_NOT_FOUND("존재하지 않는 스태프입니다"),

// 429
TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."),
Expand All @@ -63,7 +67,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,13 +2,16 @@

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;
import com.festago.entry.dto.EntryCodeResponse;
import com.festago.entry.dto.TicketValidationRequest;
import com.festago.entry.dto.TicketValidationResponse;
import com.festago.entry.dto.event.EntryProcessEvent;
import com.festago.staff.domain.Staff;
import com.festago.staff.repository.StaffRepository;
import com.festago.ticketing.domain.MemberTicket;
import com.festago.ticketing.repository.MemberTicketRepository;
import java.time.Clock;
Expand All @@ -25,6 +28,7 @@ public class EntryService {

private final EntryCodeManager entryCodeManager;
private final MemberTicketRepository memberTicketRepository;
private final StaffRepository staffRepository;
private final ApplicationEventPublisher publisher;
private final Clock clock;

Expand All @@ -45,11 +49,23 @@ private MemberTicket findMemberTicket(Long memberTicketId) {
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_TICKET_NOT_FOUND));
}

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

private Staff findStaff(Long staffId) {
return staffRepository.findById(staffId)
.orElseThrow(() -> new NotFoundException(ErrorCode.STAFF_NOT_FOUND));
}

private void checkPermission(Staff staff, MemberTicket memberTicket) {
if (!staff.canValidate(memberTicket)) {
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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -29,13 +31,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 FestivalResponse create(FestivalCreateRequest request) {
School school = schoolRepository.findById(request.schoolId())
.orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND));
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
@@ -1,6 +1,9 @@
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;
Expand All @@ -15,17 +18,28 @@
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/staff/member-tickets")
@RequestMapping("/staff")
@Tag(name = "스태프 요청")
@RequiredArgsConstructor
public class StaffMemberTicketController {
public class StaffController {

private final StaffAuthService staffAuthService;
private final EntryService entryService;

@PostMapping("/validation")
@PostMapping("/login")
@Operation(description = "스태프 코드로 로그인한다.", summary = "스태프 로그인")
public ResponseEntity<StaffLoginResponse> login(@RequestBody @Valid StaffLoginRequest request) {
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) {
TicketValidationResponse response = entryService.validate(request);
public ResponseEntity<TicketValidationResponse> validate(
@RequestBody @Valid TicketValidationRequest request,
@Staff Long staffId) {
TicketValidationResponse response = entryService.validate(request, staffId);
return ResponseEntity.ok()
.body(response);
}
Expand Down
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,13 +11,17 @@
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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 StaffCode 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 new StaffCode(abbreviation + code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.festago.staff.application;

import com.festago.festival.domain.Festival;
import com.festago.staff.domain.StaffCode;

public interface StaffCodeProvider {

StaffCode provide(Festival festival);
}
Loading
Loading