diff --git a/gateway/html/login.html b/gateway/html/login.html index 20ede4e..4e639b6 100644 --- a/gateway/html/login.html +++ b/gateway/html/login.html @@ -205,6 +205,13 @@ var existingToken = ssoCookie.get('sso_token') || localStorage.getItem('sso_token'); if (existingToken && existingToken !== 'undefined' && existingToken !== 'null') { if (isTokenValid(existingToken)) { + // 쿠키 재설정 (localStorage에만 있고 쿠키가 없는 경우 대비) + var existingUser = ssoCookie.get('sso_user') || localStorage.getItem('sso_user'); + var existingRefresh = ssoCookie.get('sso_refresh_token') || localStorage.getItem('sso_refresh_token'); + ssoCookie.set('sso_token', existingToken, 7); + if (existingUser) ssoCookie.set('sso_user', existingUser, 7); + if (existingRefresh) ssoCookie.set('sso_refresh_token', existingRefresh, 30); + var redirect = new URLSearchParams(location.search).get('redirect'); window.location.href = (redirect && isSafeRedirect(redirect)) ? redirect : '/'; } else { diff --git a/gateway/nginx.conf b/gateway/nginx.conf index dd797d5..531eb19 100644 --- a/gateway/nginx.conf +++ b/gateway/nginx.conf @@ -7,8 +7,10 @@ server { # ===== Gateway 자체 페이지 (포털, 로그인) ===== root /usr/share/nginx/html; - # 로그인 페이지 + # 로그인 페이지 (캐시 금지 — SSO 쿠키 재설정 로직 항상 최신 반영) location = /login { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; try_files /login.html =404; } diff --git a/system1-factory/api/controllers/dailyWorkReportController.js b/system1-factory/api/controllers/dailyWorkReportController.js index 69f1a3c..014b861 100644 --- a/system1-factory/api/controllers/dailyWorkReportController.js +++ b/system1-factory/api/controllers/dailyWorkReportController.js @@ -135,7 +135,7 @@ const getDailyWorkReports = async (req, res) => { try { const userInfo = { user_id: req.user?.user_id || req.user?.id, - role: req.user?.role || 'user' + role: (req.user?.role || req.user?.access_level || 'user').toLowerCase() }; if (!userInfo.user_id) { @@ -303,7 +303,7 @@ const updateWorkReport = async (req, res) => { const updateData = req.body; const userInfo = { user_id: req.user?.user_id || req.user?.id, - role: req.user?.role || 'user' + role: (req.user?.role || req.user?.access_level || 'user').toLowerCase() }; if (!userInfo.user_id) { diff --git a/system1-factory/api/routes/authRoutes.js b/system1-factory/api/routes/authRoutes.js index 5650f9f..b20e8e3 100644 --- a/system1-factory/api/routes/authRoutes.js +++ b/system1-factory/api/routes/authRoutes.js @@ -137,9 +137,11 @@ router.post('/refresh-token', async (req, res) => { const connection = await getDb(); - // 사용자 정보 조회 + // 사용자 정보 조회 (roles 조인으로 role_name 포함) const [users] = await connection.execute( - 'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE', + `SELECT u.*, r.name as role_name, u._access_level_old as access_level + FROM users u LEFT JOIN roles r ON u.role_id = r.id + WHERE u.user_id = ? AND u.is_active = TRUE`, [decoded.user_id] ); @@ -149,11 +151,12 @@ router.post('/refresh-token', async (req, res) => { const user = users[0]; - // 새 토큰 발급 + // 새 토큰 발급 (role 필드 포함) const newToken = jwt.sign( { user_id: user.user_id, username: user.username, + role: user.role_name || user.access_level || 'user', access_level: user.access_level, name: user.name || user.username }, diff --git a/system1-factory/api/services/dailyWorkReportService.js b/system1-factory/api/services/dailyWorkReportService.js index b29ff11..37a662b 100644 --- a/system1-factory/api/services/dailyWorkReportService.js +++ b/system1-factory/api/services/dailyWorkReportService.js @@ -128,8 +128,9 @@ const getDailyWorkReportsService = async (queryParams, userInfo) => { }); } - // 관리자 여부 확인 - const isAdmin = role === 'system' || role === 'admin'; + // 관리자 여부 확인 (대소문자 무시) + const roleLower = (role || '').toLowerCase(); + const isAdmin = roleLower === 'system' || roleLower === 'admin' || roleLower === 'system admin'; const canViewAll = isAdmin || view_all === 'true'; // 모델에 전달할 조회 옵션 객체 생성 diff --git a/system2-report/web/js/api-base.js b/system2-report/web/js/api-base.js index b0ca2b5..dbac71e 100644 --- a/system2-report/web/js/api-base.js +++ b/system2-report/web/js/api-base.js @@ -1,6 +1,18 @@ // /js/api-base.js // API 기본 설정 및 보안 유틸리티 - System 2 (신고 시스템) +// 서비스 워커 해제 (캐시 간섭으로 인한 인증 루프 방지) +if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + registrations.forEach(function(registration) { registration.unregister(); }); + }); + if (typeof caches !== 'undefined') { + caches.keys().then(function(names) { + names.forEach(function(name) { caches.delete(name); }); + }); + } +} + (function() { 'use strict'; @@ -35,10 +47,11 @@ */ window.getLoginUrl = function() { var hostname = window.location.hostname; + var t = Date.now(); if (hostname.includes('technicalkorea.net')) { - return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href); + return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href) + '&_t=' + t; } - return window.location.protocol + '//' + hostname + ':30000/login?redirect=' + encodeURIComponent(window.location.href); + return window.location.protocol + '//' + hostname + ':30000/login?redirect=' + encodeURIComponent(window.location.href) + '&_t=' + t; }; window.clearSSOAuth = function() { @@ -123,10 +136,9 @@ var response = await fetch(url, config); - // 401 Unauthorized 처리 + // 401 Unauthorized 처리 — 토큰만 정리하고 에러 throw (리다이렉트는 app-init이 처리) if (response.status === 401) { window.clearSSOAuth(); - window.location.href = window.getLoginUrl(); throw new Error('인증이 만료되었습니다.'); } diff --git a/system2-report/web/js/app-init.js b/system2-report/web/js/app-init.js index 0c79f2b..9cef057 100644 --- a/system2-report/web/js/app-init.js +++ b/system2-report/web/js/app-init.js @@ -4,6 +4,21 @@ (function() { 'use strict'; + // ===== 리다이렉트 루프 방지 ===== + var REDIRECT_KEY = '_sso_redirect_ts'; + var REDIRECT_COOLDOWN = 5000; // 5초 내 재리다이렉트 방지 + + function safeRedirectToLogin() { + var lastRedirect = parseInt(sessionStorage.getItem(REDIRECT_KEY) || '0', 10); + var now = Date.now(); + if (now - lastRedirect < REDIRECT_COOLDOWN) { + console.warn('[System2] 리다이렉트 루프 감지 — 로그인 페이지로 이동하지 않음'); + return; + } + sessionStorage.setItem(REDIRECT_KEY, String(now)); + window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; + } + // ===== 인증 함수 (api-base.js의 전역 헬퍼 활용) ===== function isLoggedIn() { var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'); @@ -28,17 +43,21 @@ // 1. 인증 확인 if (!isLoggedIn()) { clearAuthData(); - window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; + safeRedirectToLogin(); return; } var currentUser = getUser(); if (!currentUser || !currentUser.username) { clearAuthData(); - window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; + safeRedirectToLogin(); return; } + // 인증 성공 — 루프 카운터 리셋 + localStorage 백업 + sessionStorage.removeItem(REDIRECT_KEY); + var token = window.getSSOToken ? window.getSSOToken() : null; + if (token && !localStorage.getItem('sso_token')) localStorage.setItem('sso_token', token); console.log('[System2] 인증 확인:', currentUser.username); } diff --git a/system2-report/web/pages/safety/issue-detail.html b/system2-report/web/pages/safety/issue-detail.html index c920f39..2861fe0 100644 --- a/system2-report/web/pages/safety/issue-detail.html +++ b/system2-report/web/pages/safety/issue-detail.html @@ -8,8 +8,8 @@ - - + +