Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 43 additions & 4 deletions src/main/java/io/github/petty/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.petty.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.petty.users.jwt.JWTFilter;
import io.github.petty.users.jwt.JWTUtil;
import io.github.petty.users.jwt.LoginFilter;
Expand All @@ -8,8 +9,10 @@
import io.github.petty.users.repository.UsersRepository;
import io.github.petty.users.service.RefreshTokenService;
import io.github.petty.users.util.CookieUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -19,6 +22,9 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.time.LocalDateTime;
import java.util.Map;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
Expand Down Expand Up @@ -65,12 +71,45 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").authenticated()
.requestMatchers("/vision/**").authenticated()
// 관리자 전용
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/posts/update-counts").hasRole("ADMIN")
.requestMatchers("/manual-sync/**", "/embedding-batch/**").hasRole("ADMIN")

// 인증 필요 페이지
.requestMatchers("/profile/**").authenticated()
.requestMatchers("/posts/detail", "/posts/*/new", "/posts/*/edit").authenticated()
.requestMatchers("/flow/**").authenticated()
.requestMatchers("/login/**", "/oauth2/**", "/api/auth/refresh").permitAll()

// 인증 필요
.requestMatchers(HttpMethod.POST, "/api/posts").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/posts/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/posts/**").authenticated()
.requestMatchers(HttpMethod.POST, "/api/posts/{id}/comments").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/comments/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/comments/**").authenticated()
.requestMatchers(HttpMethod.POST, "/api/posts/{id}/like").authenticated()
.requestMatchers("/api/images/**").authenticated()
.requestMatchers("/api/users/me", "/api/check-displayname").authenticated()

// 기본 정책
.anyRequest().permitAll())
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
String requestURI = request.getRequestURI();

if (requestURI.startsWith("/api/")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> errorResponse = Map.of("error", "로그인이 필요합니다");

response.getWriter().write(mapper.writeValueAsString(errorResponse));
} else {
response.sendRedirect("/login");
}
}))
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import io.github.petty.users.dto.EmailVerificationRequest;
import io.github.petty.users.dto.RefreshTokenResponseDTO;
import io.github.petty.users.dto.VerifyCodeRequest;
import io.github.petty.users.entity.Users;
import io.github.petty.users.jwt.JWTUtil;
import io.github.petty.users.repository.UsersRepository;
import io.github.petty.users.service.EmailService;
import io.github.petty.users.service.RefreshTokenService;
import io.github.petty.users.service.UserService;
Expand All @@ -29,13 +31,15 @@ public class UsersApiController {
private final EmailService emailService;
private final RefreshTokenService refreshTokenService;
private final CookieUtils cookieUtils;
private final UsersRepository usersRepository;

public UsersApiController(JWTUtil jwtUtil, EmailService emailService, RefreshTokenService refreshTokenService, UserService userService, CookieUtils cookieUtils) {
public UsersApiController(JWTUtil jwtUtil, EmailService emailService, RefreshTokenService refreshTokenService, UserService userService, CookieUtils cookieUtils, UsersRepository usersRepository) {
this.jwtUtil = jwtUtil;
this.userService = userService;
this.emailService = emailService;
this.refreshTokenService = refreshTokenService;
this.cookieUtils = cookieUtils;
this.usersRepository = usersRepository;
}

@GetMapping("/users/me")
Expand All @@ -46,8 +50,11 @@ public ResponseEntity<Map<String, String>> getUserInfo() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

Users user = usersRepository.findByUsername(auth.getName());

Map<String, String> userInfo = new HashMap<>();
userInfo.put("username", auth.getName());
// TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
userInfo.put("username", user.getDisplayName());
Comment on lines +53 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사용자 조회 시 null 체크가 필요합니다.

usersRepository.findByUsername()이 null을 반환할 수 있으므로 적절한 처리가 필요합니다.

 Users user = usersRepository.findByUsername(auth.getName());
+if (user == null) {
+    return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
+}
 
 Map<String, String> userInfo = new HashMap<>();
 // TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
 userInfo.put("username", user.getDisplayName());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Users user = usersRepository.findByUsername(auth.getName());
Map<String, String> userInfo = new HashMap<>();
userInfo.put("username", auth.getName());
// TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
userInfo.put("username", user.getDisplayName());
Users user = usersRepository.findByUsername(auth.getName());
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
Map<String, String> userInfo = new HashMap<>();
// TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
userInfo.put("username", user.getDisplayName());
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/controller/UsersApiController.java around
lines 53 to 57, the result of usersRepository.findByUsername(auth.getName()) can
be null, which is not currently checked. Add a null check after fetching the
user; if the user is null, handle it appropriately by returning an error
response or throwing an exception to avoid NullPointerException later in the
code.

Comment on lines +56 to +57
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 응답 구조의 일관성을 개선하세요.

TODO 코멘트에서 언급된 대로 키 이름의 불일치가 있습니다. 프론트엔드와의 계약을 명확히 하고 일관된 네이밍을 사용하는 것이 좋습니다.

다음과 같이 명확한 키 이름을 사용하는 것을 권장합니다:

 Map<String, String> userInfo = new HashMap<>();
-// TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
-userInfo.put("username", user.getDisplayName());
+userInfo.put("displayName", user.getDisplayName());
+userInfo.put("username", user.getUsername()); // 실제 사용자명이 필요한 경우

또는 프론트엔드와 협의하여 API 계약을 명확히 정의하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
userInfo.put("username", user.getDisplayName());
Map<String, String> userInfo = new HashMap<>();
- // TODO: 프론트엔드에서 username 키로 displayName을 사용 중(키 이름 정리 필요)
- userInfo.put("username", user.getDisplayName());
+ userInfo.put("displayName", user.getDisplayName());
+ userInfo.put("username", user.getUsername()); // 실제 사용자명이 필요한 경우
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/controller/UsersApiController.java at
lines 56-57, the key "username" is used to store the displayName, causing
inconsistency with the frontend. To fix this, rename the key to "displayName" to
match the actual data or coordinate with the frontend team to agree on a
consistent key name for this field, ensuring the API response structure is clear
and consistent.

userInfo.put("role", auth.getAuthorities().iterator().next().getAuthority());

return ResponseEntity.ok(userInfo);
Expand Down
204 changes: 204 additions & 0 deletions src/main/resources/templates/error/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/defaultLayout}">
<head>
<title>페이지를 찾을 수 없습니다</title>
<th:block layout:fragment="css">
<style>
.error-container {
text-align: center;
padding: 50px 20px;
max-width: 600px;
margin: 0 auto;
}

.error-code {
font-size: 8rem;
font-weight: bold;
margin: 0;
background: linear-gradient(45deg, var(--accent-color), var(--point-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}

.error-title {
font-size: 2rem;
color: var(--text-color);
margin: 20px 0;
font-weight: 600;
}

.error-message {
font-size: 1.2rem;
color: var(--secondary-text-color);
margin-bottom: 30px;
line-height: 1.6;
}

.error-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
margin-top: 40px;
}

.btn {
padding: 12px 24px;
border-radius: 30px;
font-size: 1rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
border: none;
font-family: inherit;
display: inline-block;
}

.btn-primary {
background-color: var(--accent-color);
color: white;
}

.btn-primary:hover {
background-color: var(--button-hover-color);
transform: translateY(-2px);
box-shadow: var(--box-shadow-cute);
}

.btn-secondary {
background-color: var(--point-color);
color: white;
}

.btn-secondary:hover {
background-color: #8fa97a;
transform: translateY(-2px);
}

.cute-illustration {
font-size: 4rem;
margin: 30px 0;
opacity: 0.8;
}

.search-suggestion {
background-color: var(--card-bg-color);
border-radius: var(--border-radius-lg);
padding: 25px;
margin: 30px 0;
box-shadow: var(--box-shadow-light);
}

.search-suggestion h3 {
color: var(--accent-color);
margin-bottom: 15px;
font-size: 1.3rem;
}

.quick-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}

.quick-link {
display: block;
padding: 15px;
background-color: rgba(158, 188, 138, 0.1);
border-radius: var(--border-radius-md);
text-decoration: none;
color: var(--text-color);
transition: all 0.3s ease;
border: 2px solid transparent;
}

.quick-link:hover {
background-color: var(--point-color);
color: white;
transform: translateY(-2px);
border-color: var(--accent-color);
}

.quick-link .icon {
font-size: 1.5rem;
margin-bottom: 8px;
display: block;
}

.quick-link .title {
font-weight: 600;
margin-bottom: 5px;
}

.quick-link .desc {
font-size: 0.9rem;
opacity: 0.8;
}

@media (max-width: 768px) {
.error-code {
font-size: 6rem;
}

.error-title {
font-size: 1.5rem;
}

.error-message {
font-size: 1rem;
}

.error-actions {
flex-direction: column;
align-items: center;
}

.btn {
width: 100%;
max-width: 250px;
}
}
</style>
</th:block>
</head>
<body>
<th:block layout:fragment="content">
<main>
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-title">페이지를 찾을 수 없습니다</h1>
<div class="cute-illustration">🐕‍🦺 🔍</div>
<p class="error-message">
앗! 우리 반려동물이 길을 잃었나봐요.<br>
요청하신 페이지를 찾을 수 없습니다.
</p>
Comment on lines +173 to +180
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

사용자 경험을 개선하기 위한 네비게이션 요소가 누락되었습니다.

에러 메시지는 친근하고 적절하지만, 사용자가 메인 페이지나 이전 페이지로 돌아갈 수 있는 버튼이나 링크가 없습니다. CSS에는 .error-actions와 버튼 스타일이 정의되어 있지만 실제 HTML 요소가 누락되었습니다.

다음과 같이 네비게이션 요소를 추가하는 것을 권장합니다:

             <p class="error-message">
                 앗! 우리 반려동물이 길을 잃었나봐요.<br>
                 요청하신 페이지를 찾을 수 없습니다.
             </p>
+            
+            <div class="error-actions">
+                <a href="/" class="btn btn-primary">홈으로 돌아가기</a>
+                <button onclick="history.back()" class="btn btn-secondary">이전 페이지</button>
+            </div>
         </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-title">페이지를 찾을 수 없습니다</h1>
<div class="cute-illustration">🐕‍🦺 🔍</div>
<p class="error-message">
앗! 우리 반려동물이 길을 잃었나봐요.<br>
요청하신 페이지를 찾을 수 없습니다.
</p>
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-title">페이지를 찾을 수 없습니다</h1>
<div class="cute-illustration">🐕‍🦺 🔍</div>
<p class="error-message">
앗! 우리 반려동물이 길을 잃었나봐요.<br>
요청하신 페이지를 찾을 수 없습니다.
</p>
<div class="error-actions">
<a href="/" class="btn btn-primary">홈으로 돌아가기</a>
<button onclick="history.back()" class="btn btn-secondary">이전 페이지</button>
</div>
</div>
🤖 Prompt for AI Agents
In src/main/resources/templates/error/404.html around lines 173 to 180, the
error page lacks navigation elements for users to return to the main or previous
page. Add a div with class "error-actions" containing buttons or links for
navigation, such as a "Go to Home" button linking to the main page and a "Go
Back" button using JavaScript history.back(). This will improve user experience
by providing clear options to recover from the error.

</div>
</main>
</th:block>

<th:block layout:fragment="script">
<script>
// 페이지 로드 시 귀여운 애니메이션 효과
document.addEventListener('DOMContentLoaded', function() {
const illustration = document.querySelector('.cute-illustration');
if (illustration) {
illustration.style.opacity = '0';
illustration.style.transform = 'scale(0.8)';

setTimeout(() => {
illustration.style.transition = 'all 0.5s ease';
illustration.style.opacity = '0.8';
illustration.style.transform = 'scale(1)';
}, 200);
}
});
</script>
</th:block>
</body>
</html>