Skip to content

Commit

Permalink
merge: 어드민 인가-인증 기능 구현 #451
Browse files Browse the repository at this point in the history
  • Loading branch information
leegwichan authored Jan 5, 2025
2 parents 592180d + 0d9dbed commit 426da7b
Show file tree
Hide file tree
Showing 25 changed files with 524 additions and 8 deletions.
48 changes: 48 additions & 0 deletions backend/src/docs/asciidoc/admin.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
== 어드민 API

=== 어드민 로그인

==== curl

include::{snippets}/admin/auth/login/curl-request.adoc[]

==== request

include::{snippets}/admin/auth/login/http-request.adoc[]

request fields

include::{snippets}/admin/auth/login/request-fields.adoc[]

==== response

include::{snippets}/admin/auth/login/http-response.adoc[]

'''

=== 어드민 로그아웃

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/auth/logout/curl-request.adoc[]

==== request

include::{snippets}/admin/auth/logout/http-request.adoc[]

==== response

include::{snippets}/admin/auth/logout/http-response.adoc[]

'''

=== 밸런스 게임 콘텐츠들 조회

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/balance/get/curl-request.adoc[]
Expand All @@ -26,6 +66,8 @@ include::{snippets}/admin/balance/get/response-fields.adoc[]

=== 밸런스 게임 콘텐츠 생성

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/balance/create/curl-request.adoc[]
Expand All @@ -50,6 +92,8 @@ include::{snippets}/admin/balance/create/response-fields.adoc[]

=== 밸런스 게임 콘텐츠 수정

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/balance/patch-content/curl-request.adoc[]
Expand All @@ -74,6 +118,8 @@ include::{snippets}/admin/balance/patch-content/response-fields.adoc[]

=== 밸런스 게임 선택지 수정

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/balance/patch-option/curl-request.adoc[]
Expand All @@ -98,6 +144,8 @@ include::{snippets}/admin/balance/patch-option/response-fields.adoc[]

=== 밸런스 게임 선택지 삭제

CAUTION: 해당 API는 '어드민 로그인'으로 세션을 발급 받은 후에 이용해야 합니다

==== curl

include::{snippets}/admin/balance/delete/curl-request.adoc[]
Expand Down
19 changes: 19 additions & 0 deletions backend/src/main/java/ddangkong/config/AdminAuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ddangkong.config;

import ddangkong.controller.admin.AdminAuthorizationInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class AdminAuthConfig implements WebMvcConfigurer {

private final AdminAuthorizationInterceptor adminAuthorizationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminAuthorizationInterceptor);
}
}
14 changes: 14 additions & 0 deletions backend/src/main/java/ddangkong/controller/admin/Admin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ddangkong.controller.admin;

import java.io.Serializable;
import lombok.Getter;

@Getter
public class Admin implements Serializable {

private final String nickname;

public Admin(String nickname) {
this.nickname = nickname;
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/ddangkong/controller/admin/AdminAuth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ddangkong.controller.admin;

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

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AdminAuth {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ddangkong.controller.admin;

import ddangkong.exception.admin.NotExistAdminSessionException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

@Component
public class AdminAuthorizationInterceptor implements HandlerInterceptor {

private final String sessionKey;

public AdminAuthorizationInterceptor(@Value("${admin.session-key}") String sessionKey) {
this.sessionKey = sessionKey;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (hasAdminAuthAnnotation(handler)) {
authorizeAdmin(request);
}
return true;
}

private boolean hasAdminAuthAnnotation(Object handler) {
if (handler instanceof ResourceHttpRequestHandler) {
return false;
}

HandlerMethod handlerMethod = (HandlerMethod) handler;
return handlerMethod.getMethodAnnotation(AdminAuth.class) != null ||
handlerMethod.getBeanType().getAnnotation(AdminAuth.class) != null;
}

private void authorizeAdmin(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(sessionKey) == null) {
throw new NotExistAdminSessionException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ddangkong.controller.admin;

import ddangkong.facade.admin.AdminService;
import ddangkong.facade.admin.dto.AdminLoginRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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;

@Slf4j
@RestController
@RequestMapping("/api")
public class AdminController {

private final AdminService adminService;

private final String sessionKey;

public AdminController(AdminService adminService, @Value("${admin.session-key}") String sessionKey) {
this.adminService = adminService;
this.sessionKey = sessionKey;
}

@PostMapping("/admin/login")
public void login(@RequestBody AdminLoginRequest loginRequest, HttpServletRequest httpRequest) {
adminService.validatePassword(loginRequest);

Admin admin = new Admin(loginRequest.nickname());
HttpSession session = httpRequest.getSession();
session.setAttribute(sessionKey, admin);
log.info("어드민이 로그인 했습니다. nickname = {}, session = {}", loginRequest.nickname(), session.getId());
}

@AdminAuth
@PostMapping("/admin/logout")
public void logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
Admin admin = (Admin) session.getAttribute(sessionKey);
session.invalidate();
log.info("어드민이 로그아웃 했습니다. nickname = {}, session = {}", admin.getNickname(), session.getId());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package ddangkong.controller.balance;

import ddangkong.controller.admin.AdminAuth;
import ddangkong.domain.balance.content.Category;
import ddangkong.facade.balance.AdminBalanceContentFacade;
import ddangkong.facade.balance.dto.BalanceContentCreateResponse;
import ddangkong.facade.balance.dto.BalanceContentCreateRequest;
import ddangkong.facade.balance.dto.BalanceContentCreateResponse;
import ddangkong.facade.balance.dto.BalanceContentPatchRequest;
import ddangkong.facade.balance.dto.BalanceContentPatchResponse;
import ddangkong.facade.balance.dto.BalanceContentsAdminResponse;
Expand All @@ -29,27 +30,32 @@ public class AdminBalanceController {

private final AdminBalanceContentFacade adminBalanceContentFacade;

@AdminAuth
@GetMapping("/admin/balances/contents")
public BalanceContentsAdminResponse getContents(@RequestParam Category category) {
return adminBalanceContentFacade.getContents(category);
}

@AdminAuth
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/admin/balances/contents")
public BalanceContentCreateResponse createContent(@RequestBody BalanceContentCreateRequest request) {
return adminBalanceContentFacade.createContent(request);
}

@AdminAuth
@PatchMapping("/admin/balances/contents")
public BalanceContentPatchResponse updateContent(@RequestBody BalanceContentPatchRequest request) {
return adminBalanceContentFacade.updateContent(request);
}

@AdminAuth
@PatchMapping("/admin/balances/options")
public BalanceOptionPatchResponse updateOption(@RequestBody BalanceOptionPatchRequest request) {
return adminBalanceContentFacade.updateOption(request);
}

@AdminAuth
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/admin/balances/contents/{contentId}")
public void deleteContent(@PathVariable long contentId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import ddangkong.exception.BadRequestException;
import ddangkong.exception.ClientErrorCode;
import ddangkong.exception.UnauthorizedException;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Path;
import java.util.List;
Expand All @@ -24,6 +25,10 @@ public <T extends BadRequestException> ErrorResponse(T e) {
this(e.getErrorCode(), e.getMessage(), null, null);
}

public <T extends UnauthorizedException> ErrorResponse(T e) {
this(e.getErrorCode(), e.getMessage(), null, null);
}

public ErrorResponse(ClientErrorCode errorCode) {
this(errorCode, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ddangkong.exception.BadRequestException;
import ddangkong.exception.ClientErrorCode;
import ddangkong.exception.InternalServerException;
import ddangkong.exception.UnauthorizedException;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
Expand Down Expand Up @@ -77,6 +78,15 @@ public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e)
return new ErrorResponse(ClientErrorCode.NO_RESOURCE_FOUND);
}

@ExceptionHandler
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleUnauthorizedException(UnauthorizedException e) {
log.warn(e.getMessage());

return new ErrorResponse(e);
}


@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public enum ClientErrorCode {
INVALID_COOKIE("유효하지 않는 쿠키입니다."),

// ADMIN TODO Enum 분리, Docs 분리
NOT_MATCH_ADMIN_PASSWORD("어드민 비밀번호가 일치하지 않습니다."),
NOT_EXIST_ADMIN_SESSION("어드민으로 로그인하지 않았습니다."),
BLANK_BALANCE_CONTENT_NAME("컨텐츠 이름이 비어 있습니다."),
LONG_BALANCE_CONTENT_NAME("컨텐츠 이름은 최대 %d자 입니다."),
BLANK_BALANCE_OPTION_NAME("컨텐츠 옵션이 비어 있습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ddangkong.exception;

public abstract class UnauthorizedException extends RuntimeException {

public UnauthorizedException(String message) {
super(message);
}

public abstract String getErrorCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ddangkong.exception.admin;

import static ddangkong.exception.ClientErrorCode.NOT_EXIST_ADMIN_SESSION;

import ddangkong.exception.UnauthorizedException;

public class NotExistAdminSessionException extends UnauthorizedException {

public NotExistAdminSessionException() {
super(NOT_EXIST_ADMIN_SESSION.getMessage());
}

@Override
public String getErrorCode() {
return NOT_EXIST_ADMIN_SESSION.name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ddangkong.exception.admin;

import static ddangkong.exception.ClientErrorCode.NOT_MATCH_ADMIN_PASSWORD;

import ddangkong.exception.BadRequestException;

public class NotMatchAdminPasswordException extends BadRequestException {

public NotMatchAdminPasswordException() {
super(NOT_MATCH_ADMIN_PASSWORD.getMessage());
}

@Override
public String getErrorCode() {
return NOT_MATCH_ADMIN_PASSWORD.name();
}
}
22 changes: 22 additions & 0 deletions backend/src/main/java/ddangkong/facade/admin/AdminService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ddangkong.facade.admin;

import ddangkong.exception.admin.NotMatchAdminPasswordException;
import ddangkong.facade.admin.dto.AdminLoginRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class AdminService {

private final String adminPassword;

public AdminService(@Value("${admin.password}") String adminPassword) {
this.adminPassword = adminPassword;
}

public void validatePassword(AdminLoginRequest request) {
if (!adminPassword.equals(request.password())) {
throw new NotMatchAdminPasswordException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ddangkong.facade.admin.dto;

public record AdminLoginRequest(String nickname, String password) {
}
Loading

0 comments on commit 426da7b

Please sign in to comment.