수정 수정 수정

This commit is contained in:
chpark 2025-09-25 16:23:32 +09:00
parent 4c9b9a2504
commit de2c56518f
10 changed files with 190 additions and 75 deletions

View File

@ -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;

View File

@ -68,6 +68,8 @@ echo "=========================================="
echo "완료 시간: $(date)"
echo "✅ Git 최신 소스 업데이트가 완료되었습니다!"
# stash 목록이 있으면 알림
STASH_COUNT=$(git stash list | wc -l)
if [ $STASH_COUNT -gt 0 ]; then

View File

@ -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;
}
// 비동기로 로그 저장 (응답 속도에 영향 주지 않도록)

View File

@ -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;
}

View File

@ -926,22 +926,22 @@ curl -X DELETE http://localhost:5577/api/users/user005 \
<!-- 빠른 테스트 예제 -->
<div class="quick-tests">
<h3>빠른 테스트 예제</h3>
<h3>빠른 테스트 예제 - User CRUD</h3>
<div class="test-examples">
<button onclick="loadExample('getData')" class="btn btn-info btn-sm">
<i class="fas fa-download"></i> 데이터 조회 (GET)
<button onclick="loadExample('getAllUsers')" class="btn btn-info btn-sm">
<i class="fas fa-users"></i> 모든 사용자 조회 (GET)
</button>
<button onclick="loadExample('createData')" class="btn btn-success btn-sm">
<i class="fas fa-plus"></i> 데이터 생성 (POST)
<button onclick="loadExample('getUser')" class="btn btn-primary btn-sm">
<i class="fas fa-user"></i> 특정 사용자 조회 (GET)
</button>
<button onclick="loadExample('updateData')" class="btn btn-warning btn-sm">
<i class="fas fa-edit"></i> 데이터 수정 (PUT)
<button onclick="loadExample('createUser')" class="btn btn-success btn-sm">
<i class="fas fa-user-plus"></i> 사용자 생성 (POST)
</button>
<button onclick="loadExample('deleteData')" class="btn btn-danger btn-sm">
<i class="fas fa-trash"></i> 데이터 삭제 (DELETE)
<button onclick="loadExample('updateUser')" class="btn btn-warning btn-sm">
<i class="fas fa-user-edit"></i> 사용자 수정 (PUT)
</button>
<button onclick="loadExample('getUserData')" class="btn btn-primary btn-sm">
<i class="fas fa-user"></i> 사용자 데이터 조회
<button onclick="loadExample('deleteUser')" class="btn btn-danger btn-sm">
<i class="fas fa-user-times"></i> 사용자 삭제 (DELETE)
</button>
</div>
</div>

View File

@ -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) {
</div>
</div>
</td>
<td>${Array.isArray(key.permissions) ? key.permissions.join(', ') : (key.permissions || '없음')}</td>
<td>
<div class="permissions-list">
${Array.isArray(key.permissions) && key.permissions.length > 0
? key.permissions.map(p => `<span class="permission-badge">${getPermissionLabel(p)}</span>`).join(' ')
: '<span class="permission-none">권한 없음</span>'
}
</div>
</td>
<td>${key.usageCount || 0}</td>
<td>${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '사용 안함'}</td>
<td>${new Date(key.createdAt).toLocaleString()}</td>
@ -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;
}
}

View File

@ -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);

View File

@ -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(),

View File

@ -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);

View File

@ -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\// // 인증 관련 경로도 제외
]));
// 요청 로깅 미들웨어