From de2c56518fd79ceb149557545967be304882f166 Mon Sep 17 00:00:00 2001 From: chpark Date: Thu, 25 Sep 2025 16:23:32 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/auth-queries.js | 41 +++++++++++---- git_update.sh | 2 + middleware/logger.js | 2 +- public/css/style.css | 42 +++++++++++++++ public/index.html | 22 ++++---- public/js/app.js | 111 ++++++++++++++++++++++++++------------- routes/api.js | 10 ++-- routes/auth.js | 7 +-- routes/logs.js | 6 +-- server.js | 22 ++++++-- 10 files changed, 190 insertions(+), 75 deletions(-) diff --git a/database/auth-queries.js b/database/auth-queries.js index fdb1409..6358072 100644 --- a/database/auth-queries.js +++ b/database/auth-queries.js @@ -1,5 +1,6 @@ const { getConnection } = require('./connection'); const crypto = require('crypto'); +const oracledb = require('oracledb'); // 사용자 생성 async function createUser(userData) { @@ -177,19 +178,37 @@ async function getUserApiKeys(userId) { FROM API_KEYS WHERE USER_ID = :userId ORDER BY CREATED_AT DESC`, - [userId] + [userId], + { + fetchInfo: { + "PERMISSIONS": { type: oracledb.STRING } // CLOB을 문자열로 변환 + } + } ); - return result.rows.map(row => ({ - id: row[0], - keyName: row[1], - apiKey: row[2], - permissions: typeof row[3] === 'string' ? JSON.parse(row[3] || '[]') : (row[3] || []), - usageCount: row[4], - lastUsed: row[5], - createdAt: row[6], - isActive: row[7] - })); + return result.rows.map(row => { + let permissions = []; + try { + // CLOB이 문자열로 변환되어 있어야 함 + if (row[3] && typeof row[3] === 'string') { + permissions = JSON.parse(row[3]); + } + } catch (e) { + console.error('권한 파싱 오류:', row[3], e); + permissions = []; + } + + return { + id: row[0], + keyName: row[1], + apiKey: row[2], + permissions: permissions, + usageCount: row[4] || 0, + lastUsed: row[5], + createdAt: row[6], + isActive: row[7] + }; + }); } catch (err) { console.error('API 키 목록 조회 실패:', err); throw err; diff --git a/git_update.sh b/git_update.sh index 371c9f2..f636e21 100644 --- a/git_update.sh +++ b/git_update.sh @@ -68,6 +68,8 @@ echo "==========================================" echo "완료 시간: $(date)" echo "✅ Git 최신 소스 업데이트가 완료되었습니다!" + + # stash 목록이 있으면 알림 STASH_COUNT=$(git stash list | wc -l) if [ $STASH_COUNT -gt 0 ]; then diff --git a/middleware/logger.js b/middleware/logger.js index e06f83a..d87ec6d 100644 --- a/middleware/logger.js +++ b/middleware/logger.js @@ -32,7 +32,7 @@ function apiLogger(req, res, next) { // API 키 정보가 있으면 추가 if (req.apiKey) { logData.apiKeyId = req.apiKey.id; - logData.apiKeyName = req.apiKey.name; + logData.apiKeyName = req.apiKey.keyName; } // 비동기로 로그 저장 (응답 속도에 영향 주지 않도록) diff --git a/public/css/style.css b/public/css/style.css index a7f96ce..ce89e94 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1455,3 +1455,45 @@ body.admin .stats-grid .admin-only { margin-bottom: 1.5rem; font-style: italic; } + +/* 권한 배지 스타일 */ +.permissions-list { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.permission-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + border-radius: 12px; + background-color: #e3f2fd; + color: #1565c0; + border: 1px solid #bbdefb; +} + +.permission-badge:nth-child(1) { + background-color: #e8f5e8; + color: #2e7d32; + border-color: #c8e6c9; +} + +.permission-badge:nth-child(2) { + background-color: #fff3e0; + color: #ef6c00; + border-color: #ffcc02; +} + +.permission-badge:nth-child(3) { + background-color: #ffebee; + color: #c62828; + border-color: #ffcdd2; +} + +.permission-none { + color: #6c757d; + font-style: italic; + font-size: 0.875rem; +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 0975029..7c25b2f 100644 --- a/public/index.html +++ b/public/index.html @@ -926,22 +926,22 @@ curl -X DELETE http://localhost:5577/api/users/user005 \
-

빠른 테스트 예제

+

빠른 테스트 예제 - User CRUD

- - - - -
diff --git a/public/js/app.js b/public/js/app.js index 04dca39..f899390 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -94,6 +94,16 @@ function showDashboard() { // 초기 데이터 로드 loadDashboardData(); + + // API 키 사용 횟수 실시간 업데이트를 위한 주기적 새로고침 (30초마다) + if (window.apiKeyRefreshInterval) { + clearInterval(window.apiKeyRefreshInterval); + } + window.apiKeyRefreshInterval = setInterval(() => { + if (document.getElementById('apiKeysSection').style.display !== 'none') { + loadApiKeys(); + } + }, 30000); } // 토큰 검증 @@ -202,6 +212,13 @@ function handleLogout() { authToken = null; currentUser = null; document.body.classList.remove('admin'); + + // API 키 새로고침 인터벌 정리 + if (window.apiKeyRefreshInterval) { + clearInterval(window.apiKeyRefreshInterval); + window.apiKeyRefreshInterval = null; + } + showLoginScreen(); showToast('로그아웃되었습니다', 'info'); } @@ -311,7 +328,6 @@ async function loadApiKeys() { if (response.ok) { const data = await response.json(); - console.log('API 키 데이터:', data); // 디버깅용 displayApiKeys(data.apiKeys || []); } else { console.error('API 키 로드 실패:', response.status, response.statusText); @@ -325,6 +341,17 @@ async function loadApiKeys() { } } +// 권한 라벨 반환 +function getPermissionLabel(permission) { + const labels = { + 'read': '읽기', + 'write': '쓰기', + 'delete': '삭제', + 'admin': '관리자' + }; + return labels[permission] || permission; +} + // API 키 표시 function displayApiKeys(apiKeys) { const tbody = document.querySelector('#apiKeysTable tbody'); @@ -366,7 +393,14 @@ function displayApiKeys(apiKeys) { - ${Array.isArray(key.permissions) ? key.permissions.join(', ') : (key.permissions || '없음')} + +
+ ${Array.isArray(key.permissions) && key.permissions.length > 0 + ? key.permissions.map(p => `${getPermissionLabel(p)}`).join(' ') + : '권한 없음' + } +
+ ${key.usageCount || 0} ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '사용 안함'} ${new Date(key.createdAt).toLocaleString()} @@ -1308,44 +1342,47 @@ function loadExample(type) { const bodyGroup = document.getElementById('requestBodyGroup'); switch (type) { - case 'getData': - methodSelect.value = 'GET'; - urlInput.value = '/api/data'; - bodyGroup.style.display = 'none'; - break; - - case 'createData': - methodSelect.value = 'POST'; - urlInput.value = '/api/data'; - bodyTextarea.value = JSON.stringify({ - name: "테스트 데이터", - description: "API 테스터에서 생성한 테스트 데이터입니다.", - dataValue: "테스트 값" - }, null, 2); - bodyGroup.style.display = 'block'; - break; - - case 'updateData': - methodSelect.value = 'PUT'; - urlInput.value = '/api/data/1'; - bodyTextarea.value = JSON.stringify({ - name: "수정된 데이터", - description: "API 테스터에서 수정한 데이터입니다.", - dataValue: "수정된 값" - }, null, 2); - bodyGroup.style.display = 'block'; - break; - - case 'deleteData': - methodSelect.value = 'DELETE'; - urlInput.value = '/api/data/1'; - bodyGroup.style.display = 'none'; - break; - - case 'getUserData': + case 'getAllUsers': methodSelect.value = 'GET'; urlInput.value = '/api/users'; bodyGroup.style.display = 'none'; break; + + case 'getUser': + methodSelect.value = 'GET'; + urlInput.value = '/api/users/user001'; + bodyGroup.style.display = 'none'; + break; + + case 'createUser': + methodSelect.value = 'POST'; + urlInput.value = '/api/users'; + bodyTextarea.value = JSON.stringify({ + USER_ID: "user999", + USER_NAME: "홍길동", + DEPT_CODE: "IT001", + EMAIL: "hong@example.com", + PHONE: "010-1234-5678" + }, null, 2); + bodyGroup.style.display = 'block'; + break; + + case 'updateUser': + methodSelect.value = 'PUT'; + urlInput.value = '/api/users/user999'; + bodyTextarea.value = JSON.stringify({ + USER_NAME: "홍길동(수정)", + DEPT_CODE: "IT002", + EMAIL: "hong.updated@example.com", + PHONE: "010-9876-5432" + }, null, 2); + bodyGroup.style.display = 'block'; + break; + + case 'deleteUser': + methodSelect.value = 'DELETE'; + urlInput.value = '/api/users/user999'; + bodyGroup.style.display = 'none'; + break; } } diff --git a/routes/api.js b/routes/api.js index db72290..7c5b748 100644 --- a/routes/api.js +++ b/routes/api.js @@ -10,7 +10,7 @@ const { const { verifyApiKey, optionalAuth, requirePermission } = require('../middleware/auth'); // 모든 데이터 조회 (API 키 또는 JWT 토큰 필요) -router.get('/data', optionalAuth, async (req, res) => { +router.get('/data', verifyApiKey, async (req, res) => { try { const data = await getAllData(); res.json({ @@ -28,7 +28,7 @@ router.get('/data', optionalAuth, async (req, res) => { }); // 특정 ID 데이터 조회 (API 키 또는 JWT 토큰 필요) -router.get('/data/:id', optionalAuth, async (req, res) => { +router.get('/data/:id', verifyApiKey, async (req, res) => { try { const { id } = req.params; const data = await getDataById(id); @@ -55,7 +55,7 @@ router.get('/data/:id', optionalAuth, async (req, res) => { }); // 데이터 생성 (API 키 또는 JWT 토큰 필요) -router.post('/data', optionalAuth, async (req, res) => { +router.post('/data', verifyApiKey, async (req, res) => { try { const { name, description, dataValue } = req.body; @@ -87,7 +87,7 @@ router.post('/data', optionalAuth, async (req, res) => { }); // 데이터 업데이트 (API 키 또는 JWT 토큰 필요) -router.put('/data/:id', optionalAuth, async (req, res) => { +router.put('/data/:id', verifyApiKey, async (req, res) => { try { const { id } = req.params; const { name, description, dataValue } = req.body; @@ -127,7 +127,7 @@ router.put('/data/:id', optionalAuth, async (req, res) => { }); // 데이터 삭제 (API 키 또는 JWT 토큰 필요) -router.delete('/data/:id', optionalAuth, async (req, res) => { +router.delete('/data/:id', verifyApiKey, async (req, res) => { try { const { id } = req.params; const result = await deleteData(id); diff --git a/routes/auth.js b/routes/auth.js index 6d04c7a..c28c7c7 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -139,7 +139,9 @@ router.post('/api-keys', verifyToken, async (req, res) => { }); } - const result = await createApiKey(req.user.id, keyName, permissions || []); + // 기본 권한 설정 (권한이 지정되지 않은 경우) + const defaultPermissions = permissions && permissions.length > 0 ? permissions : ['read', 'write']; + const result = await createApiKey(req.user.id, keyName, defaultPermissions); res.status(201).json({ success: true, @@ -163,12 +165,11 @@ router.get('/api-keys', verifyToken, async (req, res) => { const apiKeys = await getUserApiKeys(req.user.id); // 전체 API 키를 반환 (사용자가 자신의 키를 관리할 수 있도록) - // JSON 직렬화 안전한 데이터로 변환 const safeApiKeys = apiKeys.map(key => ({ id: key.id, keyName: key.keyName, apiKey: key.apiKey, - permissions: Array.isArray(key.permissions) ? key.permissions : [], + permissions: key.permissions || [], usageCount: key.usageCount || 0, lastUsed: key.lastUsed ? new Date(key.lastUsed).toISOString() : null, createdAt: key.createdAt ? new Date(key.createdAt).toISOString() : new Date().toISOString(), diff --git a/routes/logs.js b/routes/logs.js index 9a0e2a3..ef4777e 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -1,10 +1,10 @@ const express = require('express'); const router = express.Router(); const { getApiLogs, getApiLogStats } = require('../database/log-queries'); -const { verifyToken, requirePermission } = require('../middleware/auth'); +const { verifyToken, requireAdmin } = require('../middleware/auth'); // API 로그 목록 조회 (관리자만) -router.get('/api-logs', verifyToken, requirePermission('admin'), async (req, res) => { +router.get('/api-logs', verifyToken, requireAdmin, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 50; @@ -25,7 +25,7 @@ router.get('/api-logs', verifyToken, requirePermission('admin'), async (req, res }); // API 로그 통계 조회 (관리자만) -router.get('/api-logs/stats', verifyToken, requirePermission('admin'), async (req, res) => { +router.get('/api-logs/stats', verifyToken, requireAdmin, async (req, res) => { try { const stats = await getApiLogStats(); res.json(stats); diff --git a/server.js b/server.js index 0fcf92d..a588ad0 100644 --- a/server.js +++ b/server.js @@ -22,17 +22,31 @@ app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); -// 정적 파일 서빙 -app.use(express.static(path.join(__dirname, 'public'))); +// 정적 파일 서빙 (개발 환경에서 캐시 비활성화) +app.use(express.static(path.join(__dirname, 'public'), { + etag: false, + lastModified: false, + setHeaders: (res, path) => { + // 개발 환경에서 캐시 비활성화 + if (process.env.NODE_ENV !== 'production') { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } + } +})); -// API 로깅 미들웨어 (정적 파일과 헬스체크는 제외) +// API 로깅 미들웨어 (정적 파일, 헬스체크, 로그 조회는 제외) app.use(skipLogging([ '/favicon.ico', '/api/health', + '/logs/api-logs', + '/logs/api-logs/stats', /^\/css\//, /^\/js\//, /^\/images\//, - /^\/fonts\// + /^\/fonts\//, + /^\/auth\// // 인증 관련 경로도 제외 ])); // 요청 로깅 미들웨어