Skip to content

Commit

Permalink
관리자 기능 백엔드 구현 (#780)
Browse files Browse the repository at this point in the history
* feat: spring-boot-starter-security 추가

* chore: Flyway AdminMember 테이블 추가

* feat: AdminAuth 어노테이션 구현

* feat: AdminLoginArgumentResolver 구현

* feat: AdminException 추가

* feat: AdminMember 구현

* test: AdminLoginControllerTest 작성

* feat: AdminLogin API 구현

* test: AdminServiceTest 작성

* feat: AdminService 구현

* test: AdminMemberControllerTest 구현

* feat: AdminOnly 어노테이션 구현

* feat: AdminMember API 구현

* test: AdminMemberServiceTest 작성

* feat: AdminMemberService 구현

* chore: submodule 업데이트

* feat: city 상세 목록 조회 기능 구현

* test: city 상세 목록 조회 테스트 작성

* test: city 추가 테스트 작성

* feat: city 추가 기능 구현

* test: city 수정 기능 테스트 작성

* feat: city 수정 기능 구현

* test: city 상세 목록 조회 API 테스트 작성

* feat: city 상세 목록 조회 API 구현

* feat: 도시 추가 API 구현

* test: 도시 추가 API 테스트 작성

* test: 도시 수정 API 테스트 작성

* feat: 도시 수정 API 구현

* test: 카테고리 세부 정보 조회 기능 테스트 작성

* feat: 카테고리 세부 정보 조회 기능 구현

* rename: category response dto 패키지 이동

* test: 카테고리 추가 기능 테스트 작성

* feat: 카테고리 추가 기능 구현

* test: 카테고리 수정 기능 테스트 작성

* feat: 카테고리 수정 기능 구현

* test: 카테고리 세부 정보 API 테스트 작성

* feat: 카테고리 세부 정보 API 구현

* test: 카테고리 추가 API 테스트 작성

* feat: 카테고리 추가 API 구현

* test: 카테고리 수정 API 테스트 작성

* feat: 카테고리 수정 API 구현

* test: 환율 페이징 조회 기능 테스트 작성

* feat: 환율 페이징 조회 기능 구현

* fix: 환율 조회 repository 메소드 수정

* test: 환율 조회 Api 테스트 작성

* feat: 환율 조회 Api 구현

* rename: Currency dto 패키지 이동

* test: 환율 저장 기능 테스트 작성

* feat: 환율 저장 기능 구현

* test: 환율 수정 기능 테스트 작성

* feat: 환율 수정 기능 구현

* test: 환율 생성 API 테스트 작성

* feat: 환율 생성 API 구현

* test: 환율 수정 API 테스트 작성

* feat: 환율 수정 API 구현

* test: 공백 제거

* chore: submodule 업데이트

* chore: 라이브러리 변경

* refactor: BCrypt 라이브러리 변경

* chore: 서브모듈 업데이트

* refactor: AdminMemberRepository 메소드 이름 변경

* refactor: Master static import 변경

* refactor: 패키지명 오타 수정

* refactor: 패키지 이동

* test: AdminMemberService 통합 테스트 작성

* test: AdminLoginService 통합 테스트 작성

* refactor: Category 생성, 수정 시 id를 포함하도록 변경

* test: CategoryService 통합 테스트 작성

* test: CityService 통합 테스트 작성

* test: AdminMember 생성 Controller 통합 테스트 작성

* test: AdminCurrency 생성,조회 Controller 통합 테스트 작성

* refactor: userName을 username으로 변경

* refactor: request 검증 메세지 수정

* refactor: Currency 조회 메소드 이름 변경

* refactor: response 변수 이름 변경

* refactor: 불필요한 생성자 제거

* chore: 패키지 버전 변경

* refactor: 컨벤션 맞춤

* refactor: response 생성자 private 접근제어 추가

* refactor: category 중복 제거 로직 변경

* refactor: currency 중복 검증 메소드 변경

* refactor: username 변수명 수정

* refactor: username 변수명 수정

* refactor: AdminLoginArgumentResolver 로직 변경

* refactor: Enum 개행 추가

* docs: Restdocs 추가

* refactor: 카테고리 한글명 중복 검증 제거

* refactor: 환율 request dto krw 삭제
  • Loading branch information
LJW25 authored Feb 3, 2024
1 parent 58a90de commit d3b9bd1
Show file tree
Hide file tree
Showing 83 changed files with 3,911 additions and 69 deletions.
2 changes: 1 addition & 1 deletion backend/backend-submodule
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dependencies {

implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

implementation 'org.mindrot:jbcrypt:0.4'
}

test {
Expand Down
196 changes: 196 additions & 0 deletions backend/src/docs/asciidoc/admindocs.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
= HangLog
:toc: left
:source-highlighter: highlightjs
:sectlinks:

[[overview-http-status-codes]]
=== HTTP status codes

|===
| 상태 코드 | 설명

| `200 OK`
| 성공

| `201 Created`
| 리소스 생성

| `204 NO_CONTENT`
| 성공 후 반환 값 없음

| `400 Bad Request`
| 잘못된 요청

| `401 Unauthorized`
| 비인증 상태

| `403 Forbidden`
| 권한 거부

| `404 Not Found`
| 존재하지 않는 요청 리소스

| `500 Internal Server Error`
| 서버 에러
|===

=== Exception codes
[[exception-codes]]
include::{snippets}/exception-code-controller-test/get-exception-codes/exception-response-fields.adoc[]

== 관리자 로그인 API

=== 로그인 (POST /admin/login)

==== 요청
include::{snippets}/admin-login-controller-test/login/http-request.adoc[]
include::{snippets}/admin-login-controller-test/login/request-fields.adoc[]

==== 응답
include::{snippets}/admin-login-controller-test/login/http-response.adoc[]
include::{snippets}/admin-login-controller-test/login/response-fields.adoc[]

=== 토큰 재발급 (POST /admin/token)

==== 요청
include::{snippets}/admin-login-controller-test/extend-login/http-request.adoc[]
요청 쿠키
include::{snippets}/admin-login-controller-test/extend-login/request-cookies.adoc[]

==== 응답
include::{snippets}/admin-login-controller-test/extend-login/http-response.adoc[]
include::{snippets}/admin-login-controller-test/extend-login/response-fields.adoc[]

=== 로그아웃 (DELETE /admin/logout)

==== 요청
include::{snippets}/admin-login-controller-test/logout/http-request.adoc[]
요청 헤더
include::{snippets}/admin-login-controller-test/logout/request-headers.adoc[]
요청 쿠키
include::{snippets}/admin-login-controller-test/logout/request-cookies.adoc[]

==== 응답
include::{snippets}/admin-login-controller-test/logout/http-response.adoc[]


== 관리자 멤버 관리 API

=== 관리자 멤버 목록 조회 (GET /admin/members)

==== 요청
include::{snippets}/admin-member-controller-test/get-admin-members/http-request.adoc[]

==== 응답
include::{snippets}/admin-member-controller-test/get-admin-members/http-response.adoc[]
include::{snippets}/admin-member-controller-test/get-admin-members/response-fields.adoc[]

=== 관리자 멤버 생성 (POST /admin/members)

==== 요청
include::{snippets}/admin-member-controller-test/create-admin-member/http-request.adoc[]
include::{snippets}/admin-member-controller-test/create-admin-member/request-fields.adoc[]

==== 응답
include::{snippets}/admin-member-controller-test/create-admin-member/http-response.adoc[]

=== 관리자 멤버 비밀번호 수정 (PATCH /admin/members/:memberId/password)

==== 요청
include::{snippets}/admin-member-controller-test/update-password/http-request.adoc[]
include::{snippets}/admin-member-controller-test/update-password/path-parameters.adoc[]
include::{snippets}/admin-member-controller-test/update-password/request-fields.adoc[]

==== 응답
include::{snippets}/admin-member-controller-test/update-password/http-response.adoc[]

== 도시 관리 API

=== 도시 목록 조회 (GET /admin/cities)

==== 요청
include::{snippets}/admin-city-controller-test/get-cities-detail/http-request.adoc[]

==== 응답
include::{snippets}/admin-city-controller-test/get-cities-detail/http-response.adoc[]
include::{snippets}/admin-city-controller-test/get-cities-detail/response-fields.adoc[]

=== 도시 생성 (POST /admin/cities)

==== 요청
include::{snippets}/admin-city-controller-test/create-city/http-request.adoc[]
include::{snippets}/admin-city-controller-test/create-city/request-fields.adoc[]

==== 응답
include::{snippets}/admin-city-controller-test/create-city/http-response.adoc[]

=== 도시 수정 (PUT /admin/cities/:citiId)

==== 요청
include::{snippets}/admin-city-controller-test/update-city/http-request.adoc[]
include::{snippets}/admin-city-controller-test/update-city/path-parameters.adoc[]
include::{snippets}/admin-city-controller-test/update-city/request-fields.adoc[]

==== 응답
include::{snippets}/admin-city-controller-test/update-city/http-response.adoc[]


== 카테고리 관리 API

=== 카테고리 목록 조회 (GET /admin/categories)

==== 요청
include::{snippets}/admin-category-controller-test/get-categories-detail/http-request.adoc[]

==== 응답
include::{snippets}/admin-category-controller-test/get-categories-detail/http-response.adoc[]
include::{snippets}/admin-category-controller-test/get-categories-detail/response-fields.adoc[]

=== 카테고리 생성 (POST /admin/categories)

==== 요청
include::{snippets}/admin-category-controller-test/create-category/http-request.adoc[]
include::{snippets}/admin-category-controller-test/create-category/request-fields.adoc[]

==== 응답
include::{snippets}/admin-category-controller-test/create-category/http-response.adoc[]

=== 카테고리 수정 (PUT /admin/categories/:categoryId)

==== 요청
include::{snippets}/admin-category-controller-test/update-category/http-request.adoc[]
include::{snippets}/admin-category-controller-test/update-category/path-parameters.adoc[]
include::{snippets}/admin-category-controller-test/update-category/request-fields.adoc[]

==== 응답
include::{snippets}/admin-category-controller-test/update-category/http-response.adoc[]

== 환율 관리 API

=== 환율 목록 페이지 조회 (GET /admin/currencies)

==== 요청
include::{snippets}/admin-currency-controller-test/get-currencies-detail/http-request.adoc[]

==== 응답
include::{snippets}/admin-currency-controller-test/get-currencies-detail/http-response.adoc[]
include::{snippets}/admin-currency-controller-test/get-currencies-detail/response-fields.adoc[]

=== 환율 생성 (POST /admin/currencies)

==== 요청
include::{snippets}/admin-currency-controller-test/create-currency/http-request.adoc[]
include::{snippets}/admin-currency-controller-test/create-currency/request-fields.adoc[]

==== 응답
include::{snippets}/admin-currency-controller-test/create-currency/http-response.adoc[]

=== 환율 수정 (PUT /admin/currencies/:currencyId)

==== 요청
include::{snippets}/admin-currency-controller-test/update-currency/http-request.adoc[]
include::{snippets}/admin-currency-controller-test/update-currency/path-parameters.adoc[]
include::{snippets}/admin-currency-controller-test/update-currency/request-fields.adoc[]

==== 응답
include::{snippets}/admin-currency-controller-test/update-currency/http-response.adoc[]
2 changes: 1 addition & 1 deletion backend/src/docs/asciidoc/docs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ include::{snippets}/login-controller-test/extend-login/request-fields.adoc[]
include::{snippets}/login-controller-test/extend-login/http-response.adoc[]
include::{snippets}/login-controller-test/extend-login/response-fields.adoc[]

=== 로그아웃 (POST /logout)
=== 로그아웃 (DELETE /logout)

==== 요청
include::{snippets}/login-controller-test/logout/http-request.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package hanglog.admin;

import static hanglog.admin.domain.type.AdminType.ADMIN;
import static hanglog.admin.domain.type.AdminType.MASTER;
import static hanglog.global.exception.ExceptionCode.INVALID_ADMIN_AUTHORITY;
import static hanglog.global.exception.ExceptionCode.INVALID_REQUEST;
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_REFRESH_TOKEN;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;

import hanglog.admin.domain.AdminMember;
import hanglog.admin.domain.repository.AdminMemberRepository;
import hanglog.auth.AdminAuth;
import hanglog.auth.domain.Accessor;
import hanglog.global.exception.BadRequestException;
import hanglog.global.exception.RefreshTokenException;
import hanglog.login.domain.MemberTokens;
import hanglog.login.domain.repository.RefreshTokenRepository;
import hanglog.login.infrastructure.BearerAuthorizationExtractor;
import hanglog.login.infrastructure.JwtProvider;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@RequiredArgsConstructor
@Component
public class AdminLoginArgumentResolver implements HandlerMethodArgumentResolver {

private static final String REFRESH_TOKEN = "refresh-token";

private final JwtProvider jwtProvider;

private final BearerAuthorizationExtractor extractor;

private final RefreshTokenRepository refreshTokenRepository;

private final AdminMemberRepository adminMemberRepository;

@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.withContainingClass(Long.class)
.hasParameterAnnotation(AdminAuth.class);
}

@Override
public Accessor resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory
) {
final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new BadRequestException(INVALID_REQUEST);
}

final String refreshToken = extractRefreshToken(request.getCookies());
final String accessToken = extractor.extractAccessToken(webRequest.getHeader(AUTHORIZATION));
jwtProvider.validateTokens(new MemberTokens(refreshToken, accessToken));

final Long memberId = Long.valueOf(jwtProvider.getSubject(accessToken));

final AdminMember adminMember = adminMemberRepository.findById(memberId)
.orElseThrow(() -> new RefreshTokenException(INVALID_ADMIN_AUTHORITY));

if (adminMember.getAdminType().equals(MASTER)) {
return Accessor.master(memberId);
}
if (adminMember.getAdminType().equals(ADMIN)) {
return Accessor.admin(memberId);
}
throw new RefreshTokenException(INVALID_ADMIN_AUTHORITY);
}

private String extractRefreshToken(final Cookie... cookies) {
if (cookies == null) {
throw new RefreshTokenException(NOT_FOUND_REFRESH_TOKEN);
}
return Arrays.stream(cookies)
.filter(this::isValidRefreshToken)
.findFirst()
.orElseThrow(() -> new RefreshTokenException(NOT_FOUND_REFRESH_TOKEN))
.getValue();
}

private boolean isValidRefreshToken(final Cookie cookie) {
return REFRESH_TOKEN.equals(cookie.getName()) &&
refreshTokenRepository.existsById(cookie.getValue());
}
}
19 changes: 19 additions & 0 deletions backend/src/main/java/hanglog/admin/AdminLoginResolverConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package hanglog.admin;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class AdminLoginResolverConfig implements WebMvcConfigurer {

private final AdminLoginArgumentResolver adminLoginArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(adminLoginArgumentResolver);
}
}
14 changes: 14 additions & 0 deletions backend/src/main/java/hanglog/admin/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package hanglog.admin;

import hanglog.admin.infrastructure.BCryptPasswordEncoder;
import hanglog.admin.infrastructure.PasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Loading

0 comments on commit d3b9bd1

Please sign in to comment.