diff --git a/README.md b/README.md
index a6d18c3e1..b5fbe76da 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,59 @@

+> Bottle note 프로젝트의 API 서버입니다.
-## Bottle Note Api Server
+
+
+
+
+
-### 개요(Overview)
+## *bottle note*
-> Bottle note 프로젝트의 API 서버입니다.
+---
+
+**📄*내 입맛에 딱 맞는 한병을 찾아가는 여정노트***
+
+보틀노트는 위스키 애호가들이 모여 서로의 의견을 공유하고, 더욱 즐겁고 유익한 시음 경험을 할 수 있도록 도와줍니다.
+위스키에 대한 열정을 함께 나눠보세요! 쉽고 편리하게 위스키 평가와 리뷰를 공유할 수 있어요!
+
+---
+
+**🥃 위스키 별점 및 리뷰 작성**
+
+위스키를 시음한 후 별점을 부여하고, 상세한 리뷰를 작성해 보세요!
+맛, 향, 느낌 등 다양한 측면을 기록하고 공유할 수 있습니다.
+가격을 입력하고, 시음한 장소를 태그하여 좋은 경험한 장소를 기록해 보는건 어때요?
+
+
+**🥃 위스키 정보 및 리뷰 보기**
+
+위스키의 상세 정보와 다른 사용자의 리뷰를 쉽게 확인할 수 있습니다.
+다양한 의견을 통해 다른 사람들이 위스키를 어떻게 평가했는지 알 수 있고
+새로운 위스키를 시도해볼 수 있어요!
+
+
+**🥃 찜하기 기능으로 위스키 목록 관리**
+
+관심 있는 위스키를 찜하기 기능을 통해 목록에 추가하고
+찜한 위스키 목록을 통해 나만의 위스키 컬렉션을 만들 수 있어요!
+
+**🥃 다른 유저와 팔로우 및 별점 보기**
+
+다른 사용자를 팔로우하여 그들의 별점과 리뷰를 볼 수 있어요!
+나의 팔로워들은 어떤 평가를 했을까요?
+팔로워가 나와 취향이 비슷하다면, 내 입맛에 맞는 새로운 위스키를 알 수 있을지도?
+
+
+**🥃 나의 평가 및 별점 기록 관리**
+
+내가 평가한 위스키와 부여한 별점을 한눈에 볼 수 있어요!
+나만의 위스키 평가 기록을 통해 나의 취향을 분석하고 다음 시음을 계획해보세요!
+
+
+---
+보틀노트는 위스키 애호가들이 모여 서로의 의견을 공유하고
+더욱 즐겁고 유익한 시음 경험을 할 수 있도록 도와줍니다.
-
-
+
diff --git a/build.gradle b/build.gradle
index 55fb2b284..99fac61f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -216,7 +216,7 @@ tasks.register('unitTest', Test) {
includeTags 'unit'
}
}
-tasks.register('check rule', Test) {
+tasks.register('check_rule', Test) {
useJUnitPlatform {
includeTags 'rule'
}
diff --git a/git.environment-variables b/git.environment-variables
index cd96d787a..2bf643353 160000
--- a/git.environment-variables
+++ b/git.environment-variables
@@ -1 +1 @@
-Subproject commit cd96d787a9bb9e9554ae209da068ab3060344bdd
+Subproject commit 2bf6433534696b910c69c8237689ce9753e52f9a
diff --git "a/http/\354\234\204\354\212\244\355\202\244/\354\234\204\354\212\244\355\202\244_\352\262\200\354\203\211.http" "b/http/\354\234\204\354\212\244\355\202\244/\354\234\204\354\212\244\355\202\244_\352\262\200\354\203\211.http"
index a2c9e4b8d..78959e659 100644
--- "a/http/\354\234\204\354\212\244\355\202\244/\354\234\204\354\212\244\355\202\244_\352\262\200\354\203\211.http"
+++ "b/http/\354\234\204\354\212\244\355\202\244/\354\234\204\354\212\244\355\202\244_\352\262\200\354\203\211.http"
@@ -18,13 +18,16 @@ Content-Type: application/json
%}
### 위스키 목록 조회
-
-GET {{host}}/api/v1/alcohols/search?category=SINGLE_MALT&cursor=1&pageSize=1&keyword=초콜릿
+# @no-cookie-jar
+GET {{host}}/api/v1/alcohols/search?category=SINGLE_MALT&cursor=1&pageSize=10&keyword=비 오늘 날 추천 위스키
Authorization: Bearer {{accessToken}}
+
### 위스키 조회 상세 조회
+# @no-cookie-jar
GET {{host}}/api/v1/alcohols/4
Authorization: Bearer {{accessToken}}
### 카테고리 조회
+# @no-cookie-jar
GET {{host}}/api/v1/alcohols/categories
diff --git a/src/docs/asciidoc/api/auth/auth.adoc b/src/docs/asciidoc/api/auth/auth.adoc
new file mode 100644
index 000000000..fcb8c81cf
--- /dev/null
+++ b/src/docs/asciidoc/api/auth/auth.adoc
@@ -0,0 +1,23 @@
+=== 루트 관리자 권한 검증 ===
+
+- 현재 로그인한 사용자가 루트 관리자 권한을 가지고 있는지 검증합니다.
+- 개발 / 운영 환경이 변경 가능한지 검증하는데 활용 할 수 있습니다.
+
+[source]
+----
+api/v2/auth/admin/permissions
+----
+
+[discrete]
+==== 요청 파라미터 ====
+
+[discrete]
+include::{snippets}/auth/admin/root-permissions/curl-request.adoc[]
+include::{snippets}/auth/admin/root-permissions/http-request.adoc[]
+
+[discrete]
+==== 응답 파라미터 ====
+
+[discrete]
+include::{snippets}/auth/admin/root-permissions/response-fields.adoc[]
+include::{snippets}/auth/admin/root-permissions/http-response.adoc[]
diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc
index 48b8b30bc..b206b06c9 100644
--- a/src/docs/asciidoc/index.adoc
+++ b/src/docs/asciidoc/index.adoc
@@ -84,6 +84,11 @@ include::api/overview/global-auth.adoc[]
'''
include::api/file/image/upload/presign-url.adoc[]
+== 권한 (auth) 관련 API
+
+'''
+include::api/auth/auth.adoc[]
+
== 회원 (user) 관련 API
'''
diff --git a/src/main/java/app/bottlenote/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/app/bottlenote/global/security/jwt/JwtAuthenticationFilter.java
index 9370be491..d063db410 100644
--- a/src/main/java/app/bottlenote/global/security/jwt/JwtAuthenticationFilter.java
+++ b/src/main/java/app/bottlenote/global/security/jwt/JwtAuthenticationFilter.java
@@ -154,6 +154,10 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
* 다만 내가 픽했는지 여부등을 파악하기 위해 선택적으로 제공
*
* 수정 : 2024-11-10
+ *
+ * TODO: 현재 하드코딩된 경로 관리 방식을 애노테이션 기반으로 개선 필요
+ * TODO: @AccessPolicy 또는 @OptionalAuth 애노테이션을 활용하여 컨트롤러에서 선언적으로 관리
+ * TODO: 경로 추가 시마다 두 곳(SecurityConfig + 여기)을 수정해야 하는 문제 해결 필요
*/
private boolean skipFilter(String method, String url) {
final String targetPath = method + ":" + url;
diff --git a/src/main/java/app/bottlenote/global/security/jwt/JwtProperties.java b/src/main/java/app/bottlenote/global/security/jwt/JwtProperties.java
new file mode 100644
index 000000000..66f8f8294
--- /dev/null
+++ b/src/main/java/app/bottlenote/global/security/jwt/JwtProperties.java
@@ -0,0 +1,23 @@
+package app.bottlenote.global.security.jwt;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.context.annotation.Configuration;
+
+@Setter
+@Getter
+@Configuration
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@ConfigurationProperties(prefix = "security.jwt")
+@ConfigurationPropertiesScan
+public class JwtProperties {
+ private String secretKey;
+ private long accessTokenExpiration;
+ private long refreshTokenExpiration;
+}
diff --git a/src/main/java/app/bottlenote/global/security/jwt/JwtTokenProvider.java b/src/main/java/app/bottlenote/global/security/jwt/JwtTokenProvider.java
index add3c8fe9..d208cc767 100644
--- a/src/main/java/app/bottlenote/global/security/jwt/JwtTokenProvider.java
+++ b/src/main/java/app/bottlenote/global/security/jwt/JwtTokenProvider.java
@@ -19,8 +19,8 @@
@Component
public class JwtTokenProvider {
- public static final int ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24; // 24시간
- public static final int REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14; // 14일
+ public static final int ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 2;
+ public static final int REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14;
public static final String KEY_ROLES = "roles";
private final Key secretKey;
@@ -30,7 +30,7 @@ public class JwtTokenProvider {
* @param secret 토큰 생성용 시크릿 키
*/
public JwtTokenProvider(
- @Value("${security.jwt.secret-key}") String secret
+ @Value("${security.jwt.secret-key}") String secret
) {
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
@@ -48,9 +48,9 @@ public TokenItem generateToken(String userEmail, UserType role, Long userId) {
String accessToken = createAccessToken(userEmail, role, userId);
String refreshToken = createRefreshToken(userEmail, role, userId);
return TokenItem.builder()
- .accessToken(accessToken)
- .refreshToken(refreshToken)
- .build();
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .build();
}
/**
@@ -65,11 +65,11 @@ public String createAccessToken(String userEmail, UserType role, Long userId) {
Claims claims = createClaims(userEmail, role, userId);
Date now = new Date();
return Jwts.builder()
- .setClaims(claims)
- .setIssuedAt(now)
- .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME))
- .signWith(secretKey, SignatureAlgorithm.HS512)
- .compact();
+ .setClaims(claims)
+ .setIssuedAt(now)
+ .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME))
+ .signWith(secretKey, SignatureAlgorithm.HS512)
+ .compact();
}
public String createGuestToken(Long userId, int expireTime) {
@@ -78,12 +78,12 @@ public String createGuestToken(Long userId, int expireTime) {
claims.put(KEY_ROLES, UserType.ROLE_GUEST.name());
Date now = new Date();
return Jwts.builder()
- .setClaims(claims)
- .setSubject("guest")
- .setIssuedAt(now)
- .setExpiration(new Date(now.getTime() + expireTime))
- .signWith(secretKey, SignatureAlgorithm.HS512)
- .compact();
+ .setClaims(claims)
+ .setSubject("guest")
+ .setIssuedAt(now)
+ .setExpiration(new Date(now.getTime() + expireTime))
+ .signWith(secretKey, SignatureAlgorithm.HS512)
+ .compact();
}
/**
@@ -98,11 +98,11 @@ public String createRefreshToken(String userEmail, UserType role, Long userId) {
Claims claims = createClaims(userEmail, role, userId);
Date now = new Date();
return Jwts.builder()
- .setClaims(claims)
- .setIssuedAt(now)
- .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_TIME))
- .signWith(secretKey, SignatureAlgorithm.HS512)
- .compact();
+ .setClaims(claims)
+ .setIssuedAt(now)
+ .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_TIME))
+ .signWith(secretKey, SignatureAlgorithm.HS512)
+ .compact();
}
/**
diff --git a/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java b/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java
new file mode 100644
index 000000000..cf20e64f4
--- /dev/null
+++ b/src/main/java/app/bottlenote/user/controller/AuthV2Controller.java
@@ -0,0 +1,32 @@
+package app.bottlenote.user.controller;
+
+import app.bottlenote.global.data.response.GlobalResponse;
+import app.bottlenote.global.security.SecurityContextUtil;
+import app.bottlenote.user.exception.UserException;
+import app.bottlenote.user.service.AuthService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static app.bottlenote.user.exception.UserExceptionCode.REQUIRED_USER_ID;
+
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v2/auth")
+public class AuthV2Controller {
+ private final AuthService authService;
+
+ @GetMapping("/admin/permissions")
+ public ResponseEntity> checkAdminStatus() {
+ Long currentUserId = SecurityContextUtil.getUserIdByContext().
+ orElseThrow(() -> new UserException(REQUIRED_USER_ID));
+
+ boolean is = authService.checkAdminStatus(currentUserId);
+ return GlobalResponse.ok(is);
+ }
+}
diff --git a/src/main/java/app/bottlenote/user/domain/RootAdmin.java b/src/main/java/app/bottlenote/user/domain/RootAdmin.java
new file mode 100644
index 000000000..413201702
--- /dev/null
+++ b/src/main/java/app/bottlenote/user/domain/RootAdmin.java
@@ -0,0 +1,29 @@
+package app.bottlenote.user.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.MapsId;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity(name = "root_admins")
+@Table(name = "root_admins")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class RootAdmin {
+
+ @Id
+ @Column(name = "user_id")
+ private Long userId;
+
+ @OneToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ @MapsId
+ private User user;
+}
diff --git a/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java b/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java
new file mode 100644
index 000000000..7e3a5d6c1
--- /dev/null
+++ b/src/main/java/app/bottlenote/user/repository/RootAdminRepository.java
@@ -0,0 +1,8 @@
+package app.bottlenote.user.repository;
+
+import app.bottlenote.user.domain.RootAdmin;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface RootAdminRepository extends JpaRepository