수정 수정 수정
This commit is contained in:
parent
4c9b9a2504
commit
de2c56518f
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ echo "=========================================="
|
|||
echo "완료 시간: $(date)"
|
||||
echo "✅ Git 최신 소스 업데이트가 완료되었습니다!"
|
||||
|
||||
|
||||
|
||||
# stash 목록이 있으면 알림
|
||||
STASH_COUNT=$(git stash list | wc -l)
|
||||
if [ $STASH_COUNT -gt 0 ]; then
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// 비동기로 로그 저장 (응답 속도에 영향 주지 않도록)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
111
public/js/app.js
111
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) {
|
|||
</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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
22
server.js
22
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\// // 인증 관련 경로도 제외
|
||||
]));
|
||||
|
||||
// 요청 로깅 미들웨어
|
||||
|
|
|
|||
Loading…
Reference in New Issue