From 97e5a243438a2d56e45e4cfc0abd1d7594be6ee2 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Mon, 13 Apr 2026 09:27:17 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EC=A0=9C=EA=B1=B0=20+?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=97=AD=ED=95=A0=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20=EB=B2=84=ED=8A=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JWT 인증 및 LoginPage 제거, SecurityConfig permitAll 전환 - @PreAuthorize 어노테이션 전체 제거 (@EnableMethodSecurity 비활성화) - ADMIN/MANAGER/USER 역할 토글 버튼 (헤더) + localStorage 연동 - X-User-Id 헤더 기반 사용자 식별 (ApiKeyController, ApiKeyRequestController) - RoleGuard 컴포넌트로 관리자 전용 페이지 접근 제어 - WebViewController 루트 리다이렉트 수정 (이중 context-path 방지) closes #35 --- frontend/src/App.tsx | 48 ++++------ frontend/src/components/ProtectedRoute.tsx | 21 +---- frontend/src/components/RoleGuard.tsx | 41 ++++++++ frontend/src/layouts/AuthLayout.tsx | 11 --- frontend/src/layouts/MainLayout.tsx | 34 ++++--- frontend/src/pages/LoginPage.tsx | 93 ------------------- frontend/src/services/apiClient.ts | 79 +--------------- frontend/src/services/authService.ts | 69 +------------- frontend/src/store/AuthContext.tsx | 80 ++++++---------- frontend/src/types/auth.ts | 15 +-- .../apikey/controller/ApiKeyController.java | 18 ++-- .../controller/ApiKeyRequestController.java | 15 ++- .../connection/config/SecurityConfig.java | 13 +-- .../global/controller/WebViewController.java | 10 +- .../controller/HeartbeatController.java | 2 - .../service/controller/ServiceController.java | 4 - .../tenant/controller/TenantController.java | 4 - .../user/controller/UserController.java | 5 - 18 files changed, 150 insertions(+), 412 deletions(-) create mode 100644 frontend/src/components/RoleGuard.tsx delete mode 100644 frontend/src/layouts/AuthLayout.tsx delete mode 100644 frontend/src/pages/LoginPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9f6a8c8..c9d77ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import ThemeProvider from './store/ThemeContext'; import AuthProvider from './store/AuthContext'; -import AuthLayout from './layouts/AuthLayout'; import MainLayout from './layouts/MainLayout'; -import ProtectedRoute from './components/ProtectedRoute'; -import LoginPage from './pages/LoginPage'; import DashboardPage from './pages/DashboardPage'; import RequestLogsPage from './pages/monitoring/RequestLogsPage'; import RequestLogDetailPage from './pages/monitoring/RequestLogDetailPage'; @@ -22,6 +19,7 @@ import ApiStatsPage from './pages/statistics/ApiStatsPage'; import TenantStatsPage from './pages/statistics/TenantStatsPage'; import UsageTrendPage from './pages/statistics/UsageTrendPage'; import NotFoundPage from './pages/NotFoundPage'; +import RoleGuard from './components/RoleGuard'; const BASE_PATH = '/snp-connection'; @@ -31,31 +29,25 @@ const App = () => { - }> - } /> - - - }> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index e2e53b0..c20e6c4 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -1,22 +1,5 @@ -import { Navigate, Outlet } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; +import { Outlet } from 'react-router-dom'; -const ProtectedRoute = () => { - const { isAuthenticated, isLoading } = useAuth(); - - if (isLoading) { - return ( -
-
-
- ); - } - - if (!isAuthenticated) { - return ; - } - - return ; -}; +const ProtectedRoute = () => ; export default ProtectedRoute; diff --git a/frontend/src/components/RoleGuard.tsx b/frontend/src/components/RoleGuard.tsx new file mode 100644 index 0000000..89b3a67 --- /dev/null +++ b/frontend/src/components/RoleGuard.tsx @@ -0,0 +1,41 @@ +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +interface RoleGuardProps { + allowedRoles: string[]; + children: React.ReactNode; +} + +const RoleGuard = ({ allowedRoles, children }: RoleGuardProps) => { + const { user } = useAuth(); + const navigate = useNavigate(); + + if (!user || !allowedRoles.includes(user.role)) { + return ( +
+
+
+ + + +
+

접근 권한이 없습니다

+

+ 이 페이지는 {allowedRoles.join(', ')} 권한이 필요합니다. +
현재 권한: {user?.role || '-'} +

+ +
+
+ ); + } + + return <>{children}; +}; + +export default RoleGuard; diff --git a/frontend/src/layouts/AuthLayout.tsx b/frontend/src/layouts/AuthLayout.tsx deleted file mode 100644 index 5b658cd..0000000 --- a/frontend/src/layouts/AuthLayout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Outlet } from 'react-router-dom'; - -const AuthLayout = () => { - return ( -
- -
- ); -}; - -export default AuthLayout; diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index 6c0a812..64d1a23 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -46,8 +46,10 @@ const navGroups: NavGroup[] = [ }, ]; +const ROLES = ['ADMIN', 'MANAGER', 'USER'] as const; + const MainLayout = () => { - const { user, logout } = useAuth(); + const { user, setRole } = useAuth(); const { theme, toggleTheme } = useTheme(); const [openGroups, setOpenGroups] = useState>({ Monitoring: true, @@ -60,10 +62,6 @@ const MainLayout = () => { setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] })); }; - const handleLogout = async () => { - await logout(); - }; - const isAdminOrManager = user?.role === 'ADMIN' || user?.role === 'MANAGER'; return ( @@ -170,16 +168,22 @@ const MainLayout = () => { )} - {user?.userName} - - {user?.role} - - + Role: +
+ {ROLES.map((role) => ( + + ))} +
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx deleted file mode 100644 index a5ea345..0000000 --- a/frontend/src/pages/LoginPage.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react'; -import type { FormEvent } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; - -const LoginPage = () => { - const navigate = useNavigate(); - const { login } = useAuth(); - const [loginId, setLoginId] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); - - if (!loginId.trim() || !password.trim()) { - setError('아이디와 비밀번호를 입력해주세요.'); - return; - } - - setIsSubmitting(true); - try { - await login(loginId, password); - navigate('/dashboard', { replace: true }); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } else { - setError('로그인에 실패했습니다.'); - } - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
-

- SNP Connection Monitoring -

- -
-
- - setLoginId(e.target.value)} - className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-4 py-2.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" - placeholder="아이디를 입력하세요" - autoComplete="username" - /> -
- -
- - setPassword(e.target.value)} - className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-4 py-2.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" - placeholder="비밀번호를 입력하세요" - autoComplete="current-password" - /> -
- - {error && ( -

{error}

- )} - - -
-
-
- ); -}; - -export default LoginPage; diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index 85a3462..091540d 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -2,93 +2,24 @@ import type { ApiResponse } from '../types/api'; const BASE_URL = '/snp-connection/api'; -let isRefreshing = false; -let refreshPromise: Promise | null = null; +let currentUserId: number = 1; -const getAccessToken = (): string | null => { - return localStorage.getItem('snp_access_token'); -}; - -const getRefreshToken = (): string | null => { - return localStorage.getItem('snp_refresh_token'); -}; - -const clearStorage = () => { - localStorage.removeItem('snp_access_token'); - localStorage.removeItem('snp_refresh_token'); - localStorage.removeItem('snp_user'); -}; - -const attemptRefresh = async (): Promise => { - const refreshToken = getRefreshToken(); - if (!refreshToken) return false; - - try { - const response = await fetch(`${BASE_URL}/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken }), - }); - - if (!response.ok) return false; - - const data = await response.json() as ApiResponse<{ accessToken: string }>; - if (data.success && data.data?.accessToken) { - localStorage.setItem('snp_access_token', data.data.accessToken); - return true; - } - return false; - } catch { - return false; - } -}; - -const handleTokenRefresh = async (): Promise => { - if (isRefreshing && refreshPromise) { - return refreshPromise; - } - - isRefreshing = true; - refreshPromise = attemptRefresh().finally(() => { - isRefreshing = false; - refreshPromise = null; - }); - - return refreshPromise; +export const setApiClientUserId = (userId: number) => { + currentUserId = userId; }; const request = async (url: string, options: RequestInit = {}): Promise> => { - const token = getAccessToken(); const headers: Record = { 'Content-Type': 'application/json', + 'X-User-Id': String(currentUserId), ...options.headers as Record, }; - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - let response = await fetch(`${BASE_URL}${url}`, { + const response = await fetch(`${BASE_URL}${url}`, { ...options, headers, }); - if (response.status === 401) { - const refreshed = await handleTokenRefresh(); - if (refreshed) { - const newToken = getAccessToken(); - headers['Authorization'] = `Bearer ${newToken}`; - response = await fetch(`${BASE_URL}${url}`, { - ...options, - headers, - }); - } else { - clearStorage(); - window.location.href = '/snp-connection/login'; - return { success: false, message: 'Authentication failed' }; - } - } - const data = await response.json() as ApiResponse; return data; }; diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 4e453ad..4be5913 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -1,67 +1,2 @@ -import { post } from './apiClient'; -import type { LoginRequest, LoginResponse, User } from '../types/auth'; - -const TOKEN_KEY = 'snp_access_token'; -const REFRESH_TOKEN_KEY = 'snp_refresh_token'; -const USER_KEY = 'snp_user'; - -export const login = async (req: LoginRequest): Promise => { - const response = await post('/auth/login', req); - - if (!response.success || !response.data) { - throw new Error(response.message || '로그인에 실패했습니다.'); - } - - const data = response.data; - localStorage.setItem(TOKEN_KEY, data.accessToken); - localStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken); - localStorage.setItem(USER_KEY, JSON.stringify({ - loginId: data.loginId, - userName: data.userName, - role: data.role, - })); - - return data; -}; - -export const logout = async (): Promise => { - try { - await post('/auth/logout'); - } finally { - localStorage.removeItem(TOKEN_KEY); - localStorage.removeItem(REFRESH_TOKEN_KEY); - localStorage.removeItem(USER_KEY); - } -}; - -export const refreshToken = async (): Promise => { - const storedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); - if (!storedRefreshToken) { - throw new Error('No refresh token available'); - } - - const response = await post<{ accessToken: string }>('/auth/refresh', { - refreshToken: storedRefreshToken, - }); - - if (!response.success || !response.data) { - throw new Error('Token refresh failed'); - } - - localStorage.setItem(TOKEN_KEY, response.data.accessToken); -}; - -export const getStoredUser = (): User | null => { - const userStr = localStorage.getItem(USER_KEY); - if (!userStr) return null; - - try { - return JSON.parse(userStr) as User; - } catch { - return null; - } -}; - -export const getAccessToken = (): string | null => { - return localStorage.getItem(TOKEN_KEY); -}; +// Auth service removed - login/logout no longer needed. +// Role is managed via AuthContext's setRole function. diff --git a/frontend/src/store/AuthContext.tsx b/frontend/src/store/AuthContext.tsx index a6d8ff4..fd5afd7 100644 --- a/frontend/src/store/AuthContext.tsx +++ b/frontend/src/store/AuthContext.tsx @@ -1,17 +1,13 @@ -import { createContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useState } from 'react'; import type { ReactNode } from 'react'; import type { User } from '../types/auth'; -import * as authService from '../services/authService'; +import { setApiClientUserId } from '../services/apiClient'; -interface AuthState { +interface AuthContextValue { user: User | null; isAuthenticated: boolean; isLoading: boolean; -} - -interface AuthContextValue extends AuthState { - login: (loginId: string, password: string) => Promise; - logout: () => Promise; + setRole: (role: 'ADMIN' | 'MANAGER' | 'USER') => void; } export const AuthContext = createContext(null); @@ -20,56 +16,32 @@ interface AuthProviderProps { children: ReactNode; } +const ROLE_USERS: Record = { + ADMIN: { userId: 1, loginId: 'admin', userName: '관리자', role: 'ADMIN' }, + MANAGER: { userId: 7, loginId: 'manager', userName: '매니저', role: 'MANAGER' }, + USER: { userId: 2, loginId: 'user', userName: '사용자', role: 'USER' }, +}; + +const getInitialUser = (): User => { + const savedRole = localStorage.getItem('snp-role') as 'ADMIN' | 'MANAGER' | 'USER' | null; + const u = ROLE_USERS[savedRole || 'ADMIN']; + setApiClientUserId(u.userId); + return u; +}; + const AuthProvider = ({ children }: AuthProviderProps) => { - const [state, setState] = useState({ - user: null, - isAuthenticated: false, - isLoading: true, - }); + const [user, setUser] = useState(getInitialUser); - useEffect(() => { - const storedUser = authService.getStoredUser(); - const token = authService.getAccessToken(); - - if (storedUser && token) { - setState({ - user: storedUser, - isAuthenticated: true, - isLoading: false, - }); - } else { - setState({ - user: null, - isAuthenticated: false, - isLoading: false, - }); - } - }, []); - - const login = useCallback(async (loginId: string, password: string) => { - const response = await authService.login({ loginId, password }); - setState({ - user: { - loginId: response.loginId, - userName: response.userName, - role: response.role, - }, - isAuthenticated: true, - isLoading: false, - }); - }, []); - - const logout = useCallback(async () => { - await authService.logout(); - setState({ - user: null, - isAuthenticated: false, - isLoading: false, - }); - }, []); + const setRole = (role: 'ADMIN' | 'MANAGER' | 'USER') => { + localStorage.setItem('snp-role', role); + const u = ROLE_USERS[role]; + setUser(u); + setApiClientUserId(u.userId); + window.location.reload(); + }; return ( - + {children} ); diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index 009136b..22f32b1 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -1,18 +1,5 @@ -export interface LoginRequest { - loginId: string; - password: string; -} - -export interface LoginResponse { - accessToken: string; - refreshToken: string; - loginId: string; - userName: string; - role: string; - expiresIn: number; -} - export interface User { + userId: number; loginId: string; userName: string; role: string; diff --git a/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyController.java b/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyController.java index da70468..bb804e0 100644 --- a/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyController.java +++ b/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyController.java @@ -9,10 +9,10 @@ import com.gcsc.connection.apikey.dto.UpdatePermissionsRequest; import com.gcsc.connection.apikey.service.ApiKeyPermissionService; import com.gcsc.connection.apikey.service.ApiKeyService; import com.gcsc.connection.common.dto.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -34,6 +34,7 @@ public class ApiKeyController { private final ApiKeyService apiKeyService; private final ApiKeyPermissionService apiKeyPermissionService; + private final HttpServletRequest request; /** * 내 API Key 목록 조회 @@ -49,7 +50,6 @@ public class ApiKeyController { * 전체 API Key 목록 조회 (관리자용) */ @GetMapping("/all") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity>> getAllKeys() { List keys = apiKeyService.getAllKeys(); return ResponseEntity.ok(ApiResponse.ok(keys)); @@ -59,7 +59,6 @@ public class ApiKeyController { * API Key 상세 조회 (복호화된 키 포함, 관리자 전용) */ @GetMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getKeyDetail(@PathVariable Long id) { ApiKeyDetailResponse detail = apiKeyService.getKeyDetail(id); return ResponseEntity.ok(ApiResponse.ok(detail)); @@ -69,7 +68,6 @@ public class ApiKeyController { * API Key 생성 (관리자 전용) */ @PostMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> createKey( @RequestBody @Valid CreateApiKeyRequest request) { Long userId = getCurrentUserId(); @@ -99,7 +97,6 @@ public class ApiKeyController { * API Key 권한 수정 (관리자/매니저 전용) */ @PutMapping("/{id}/permissions") - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity>> updatePermissions( @PathVariable Long id, @RequestBody @Valid UpdatePermissionsRequest request) { @@ -109,6 +106,15 @@ public class ApiKeyController { } private Long getCurrentUserId() { - return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); + String userIdHeader = request.getHeader("X-User-Id"); + if (userIdHeader != null && !userIdHeader.isBlank()) { + return Long.parseLong(userIdHeader); + } + // fallback: SecurityContext + try { + return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); + } catch (Exception e) { + return 1L; // default admin + } } } diff --git a/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyRequestController.java b/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyRequestController.java index 200d771..373bb83 100644 --- a/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyRequestController.java +++ b/src/main/java/com/gcsc/connection/apikey/controller/ApiKeyRequestController.java @@ -6,10 +6,10 @@ import com.gcsc.connection.apikey.dto.ApiKeyRequestResponse; import com.gcsc.connection.apikey.dto.ApiKeyRequestReviewDto; import com.gcsc.connection.apikey.service.ApiKeyRequestService; import com.gcsc.connection.common.dto.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -31,6 +31,7 @@ import java.util.List; public class ApiKeyRequestController { private final ApiKeyRequestService apiKeyRequestService; + private final HttpServletRequest request; /** * API Key 신청 생성 @@ -57,7 +58,6 @@ public class ApiKeyRequestController { * 신청 목록 조회 (관리자/매니저용) */ @GetMapping - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity>> getRequests( @RequestParam(required = false) String status) { List responses; @@ -73,7 +73,6 @@ public class ApiKeyRequestController { * 신청 심사 (승인/거절) */ @PutMapping("/{id}/review") - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity> reviewRequest( @PathVariable Long id, @RequestBody @Valid ApiKeyRequestReviewDto request) { @@ -86,6 +85,14 @@ public class ApiKeyRequestController { } private Long getCurrentUserId() { - return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); + String userIdHeader = request.getHeader("X-User-Id"); + if (userIdHeader != null && !userIdHeader.isBlank()) { + return Long.parseLong(userIdHeader); + } + try { + return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); + } catch (Exception e) { + return 1L; + } } } diff --git a/src/main/java/com/gcsc/connection/config/SecurityConfig.java b/src/main/java/com/gcsc/connection/config/SecurityConfig.java index dcc4423..5f4d405 100644 --- a/src/main/java/com/gcsc/connection/config/SecurityConfig.java +++ b/src/main/java/com/gcsc/connection/config/SecurityConfig.java @@ -1,7 +1,5 @@ package com.gcsc.connection.config; -import com.gcsc.connection.auth.jwt.JwtAuthenticationFilter; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -13,16 +11,12 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity -@EnableMethodSecurity -@RequiredArgsConstructor +// @EnableMethodSecurity -- disabled (no login) public class SecurityConfig { - private final JwtAuthenticationFilter jwtAuthenticationFilter; - @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -42,9 +36,8 @@ public class SecurityConfig { .requestMatchers("/actuator/**").permitAll() .requestMatchers("/", "/*.html", "/assets/**", "/favicon*", "/site.webmanifest").permitAll() .requestMatchers("/gateway/**").permitAll() - .requestMatchers("/api/**").authenticated() - .anyRequest().permitAll()) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .requestMatchers("/api/**").permitAll() + .anyRequest().permitAll()); return http.build(); } diff --git a/src/main/java/com/gcsc/connection/global/controller/WebViewController.java b/src/main/java/com/gcsc/connection/global/controller/WebViewController.java index eedf791..d039175 100644 --- a/src/main/java/com/gcsc/connection/global/controller/WebViewController.java +++ b/src/main/java/com/gcsc/connection/global/controller/WebViewController.java @@ -12,8 +12,14 @@ import org.springframework.web.bind.annotation.GetMapping; @Controller public class WebViewController { - @GetMapping({"/", "/login", "/dashboard", "/dashboard/**", - "/monitoring/**", "/apikeys", "/apikeys/**", + @GetMapping("/") + public String root() { + return "redirect:/dashboard"; + } + + @GetMapping({"/dashboard", "/dashboard/**", + "/monitoring/**", "/statistics/**", + "/apikeys", "/apikeys/**", "/admin/**"}) public String forward() { return "forward:/index.html"; diff --git a/src/main/java/com/gcsc/connection/monitoring/controller/HeartbeatController.java b/src/main/java/com/gcsc/connection/monitoring/controller/HeartbeatController.java index 620fbad..53be9f5 100644 --- a/src/main/java/com/gcsc/connection/monitoring/controller/HeartbeatController.java +++ b/src/main/java/com/gcsc/connection/monitoring/controller/HeartbeatController.java @@ -7,7 +7,6 @@ import com.gcsc.connection.monitoring.dto.ServiceStatusDetailResponse; import com.gcsc.connection.monitoring.service.HeartbeatService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -68,7 +67,6 @@ public class HeartbeatController { * 수동 헬스체크 실행 */ @PostMapping("/{serviceId}/check") - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity> checkService( @PathVariable Long serviceId) { HeartbeatStatusResponse result = heartbeatService.checkService(serviceId); diff --git a/src/main/java/com/gcsc/connection/service/controller/ServiceController.java b/src/main/java/com/gcsc/connection/service/controller/ServiceController.java index b5ee865..86d4417 100644 --- a/src/main/java/com/gcsc/connection/service/controller/ServiceController.java +++ b/src/main/java/com/gcsc/connection/service/controller/ServiceController.java @@ -10,7 +10,6 @@ import com.gcsc.connection.service.service.ServiceManagementService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -44,7 +43,6 @@ public class ServiceController { * 서비스 생성 */ @PostMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> createService( @RequestBody @Valid CreateServiceRequest request) { ServiceResponse service = serviceManagementService.createService(request); @@ -55,7 +53,6 @@ public class ServiceController { * 서비스 수정 */ @PutMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> updateService( @PathVariable Long id, @RequestBody @Valid UpdateServiceRequest request) { @@ -77,7 +74,6 @@ public class ServiceController { * 서비스 API 생성 */ @PostMapping("/{id}/apis") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> createServiceApi( @PathVariable Long id, @RequestBody @Valid CreateServiceApiRequest request) { diff --git a/src/main/java/com/gcsc/connection/tenant/controller/TenantController.java b/src/main/java/com/gcsc/connection/tenant/controller/TenantController.java index bb401d1..bf0a3da 100644 --- a/src/main/java/com/gcsc/connection/tenant/controller/TenantController.java +++ b/src/main/java/com/gcsc/connection/tenant/controller/TenantController.java @@ -8,7 +8,6 @@ import com.gcsc.connection.tenant.service.TenantService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -33,7 +32,6 @@ public class TenantController { * 전체 테넌트 목록 조회 */ @GetMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity>> getTenants() { List tenants = tenantService.getTenants(); return ResponseEntity.ok(ApiResponse.ok(tenants)); @@ -43,7 +41,6 @@ public class TenantController { * 테넌트 생성 */ @PostMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> createTenant( @RequestBody @Valid CreateTenantRequest request) { TenantResponse tenant = tenantService.createTenant(request); @@ -54,7 +51,6 @@ public class TenantController { * 테넌트 수정 */ @PutMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> updateTenant( @PathVariable Long id, @RequestBody @Valid UpdateTenantRequest request) { diff --git a/src/main/java/com/gcsc/connection/user/controller/UserController.java b/src/main/java/com/gcsc/connection/user/controller/UserController.java index bee9496..83d0324 100644 --- a/src/main/java/com/gcsc/connection/user/controller/UserController.java +++ b/src/main/java/com/gcsc/connection/user/controller/UserController.java @@ -9,7 +9,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -36,7 +35,6 @@ public class UserController { * 전체 사용자 목록 조회 */ @GetMapping - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity>> getUsers() { List users = userService.getUsers(); return ResponseEntity.ok(ApiResponse.ok(users)); @@ -46,7 +44,6 @@ public class UserController { * 사용자 생성 */ @PostMapping - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> createUser( @RequestBody @Valid CreateUserRequest request) { UserResponse user = userService.createUser(request); @@ -57,7 +54,6 @@ public class UserController { * 사용자 단건 조회 (ADMIN/MANAGER 또는 본인) */ @GetMapping("/{id}") - @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") public ResponseEntity> getUser(@PathVariable Long id) { String currentUserId = SecurityContextHolder.getContext().getAuthentication().getName(); if (!isAdminOrManager() && !currentUserId.equals(String.valueOf(id))) { @@ -86,7 +82,6 @@ public class UserController { * 사용자 비활성화 (소프트 삭제) */ @DeleteMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> deactivateUser(@PathVariable Long id) { userService.deactivateUser(id); return ResponseEntity.ok(ApiResponse.ok(null, "사용자가 비활성화되었습니다")); -- 2.45.2 From 765d0e01c6abb4d3b2288f1302464174b83d3698 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Mon, 13 Apr 2026 09:27:31 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20Request=20Logs=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EA=B0=84=20=ED=94=84=EB=A6=AC=EC=85=8B=20+=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20UI=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 날짜 프리셋 버튼 추가 (오늘/어제/최근7일/이번달/지난달/직접선택) - 필터 영역 한 줄로 통합 (서비스/상태/Method/검색/초기화) - IP 입력 필드 제거 --- .../src/pages/monitoring/RequestLogsPage.tsx | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/frontend/src/pages/monitoring/RequestLogsPage.tsx b/frontend/src/pages/monitoring/RequestLogsPage.tsx index 3e0e6c3..0e889dd 100644 --- a/frontend/src/pages/monitoring/RequestLogsPage.tsx +++ b/frontend/src/pages/monitoring/RequestLogsPage.tsx @@ -26,14 +26,13 @@ const REQUEST_STATUSES = ['SUCCESS', 'FAIL', 'DENIED', 'EXPIRED', 'INVALID_KEY', const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE']; const DEFAULT_PAGE_SIZE = 20; -const getTodayString = (): string => { - const d = new Date(); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; +const formatDate = (d: Date): string => { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; +const getToday = (): string => formatDate(new Date()); +const getTodayString = getToday; + const formatDateTime = (dateStr: string): string => { const d = new Date(dateStr); const year = d.getFullYear(); @@ -50,10 +49,10 @@ const RequestLogsPage = () => { const [startDate, setStartDate] = useState(getTodayString()); const [endDate, setEndDate] = useState(getTodayString()); + const [datePreset, setDatePreset] = useState('오늘'); const [serviceId, setServiceId] = useState(''); const [requestStatus, setRequestStatus] = useState(''); const [requestMethod, setRequestMethod] = useState(''); - const [requestIp, setRequestIp] = useState(''); const [services, setServices] = useState([]); const [result, setResult] = useState | null>(null); @@ -84,7 +83,6 @@ const RequestLogsPage = () => { serviceId: serviceId ? Number(serviceId) : undefined, requestStatus: requestStatus || undefined, requestMethod: requestMethod || undefined, - requestIp: requestIp || undefined, page, size: DEFAULT_PAGE_SIZE, }; @@ -104,10 +102,10 @@ const RequestLogsPage = () => { const handleReset = () => { setStartDate(getTodayString()); setEndDate(getTodayString()); + setDatePreset('오늘'); setServiceId(''); setRequestStatus(''); setRequestMethod(''); - setRequestIp(''); setCurrentPage(0); }; @@ -169,45 +167,68 @@ const RequestLogsPage = () => { {/* Search Form */}
-
+
+
+ {([ + { label: '오늘', fn: () => { const t = getToday(); setStartDate(t); setEndDate(t); setDatePreset('오늘'); } }, + { label: '어제', fn: () => { const d = new Date(); d.setDate(d.getDate() - 1); const y = formatDate(d); setStartDate(y); setEndDate(y); setDatePreset('어제'); } }, + { label: '최근 7일', fn: () => { const d = new Date(); d.setDate(d.getDate() - 6); setStartDate(formatDate(d)); setEndDate(getToday()); setDatePreset('최근 7일'); } }, + { label: '이번 달', fn: () => { const d = new Date(); setStartDate(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`); setEndDate(getToday()); setDatePreset('이번 달'); } }, + { label: '지난 달', fn: () => { const d = new Date(); d.setMonth(d.getMonth() - 1); const s = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`; const e = new Date(d.getFullYear(), d.getMonth() + 1, 0); setStartDate(s); setEndDate(formatDate(e)); setDatePreset('지난 달'); } }, + { label: '직접 선택', fn: () => { setDatePreset('직접 선택'); } }, + ]).map((btn) => ( + + ))} +
setStartDate(e.target.value)} + onChange={(e) => { setStartDate(e.target.value); setDatePreset('직접 선택'); }} className="flex-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" /> ~ setEndDate(e.target.value)} + onChange={(e) => { setEndDate(e.target.value); setDatePreset('직접 선택'); }} className="flex-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" />
+
+
- +
- +
-
-
- +
-
- - setRequestIp(e.target.value)} - placeholder="IP 주소" - className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" - /> -
-
+