422 lines
11 KiB
JavaScript
422 lines
11 KiB
JavaScript
|
|
const { getConnection } = require('./connection');
|
||
|
|
const crypto = require('crypto');
|
||
|
|
|
||
|
|
// 사용자 생성
|
||
|
|
async function createUser(userData) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
// 비밀번호 해시화
|
||
|
|
const salt = crypto.randomBytes(32).toString('hex');
|
||
|
|
const hashedPassword = crypto.pbkdf2Sync(userData.password, salt, 10000, 64, 'sha512').toString('hex');
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`INSERT INTO USERS (USERNAME, EMAIL, PASSWORD_HASH, SALT, ROLE)
|
||
|
|
VALUES (:username, :email, :passwordHash, :salt, :role)`,
|
||
|
|
{
|
||
|
|
username: userData.username,
|
||
|
|
email: userData.email,
|
||
|
|
passwordHash: hashedPassword,
|
||
|
|
salt: salt,
|
||
|
|
role: userData.role || 'user'
|
||
|
|
},
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
|
||
|
|
return result.rowsAffected;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('사용자 생성 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 사용자 인증
|
||
|
|
async function authenticateUser(username, password) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`SELECT ID, USERNAME, EMAIL, PASSWORD_HASH, SALT, ROLE, IS_ACTIVE
|
||
|
|
FROM USERS WHERE USERNAME = :username AND IS_ACTIVE = 1`,
|
||
|
|
[username]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = {
|
||
|
|
id: result.rows[0][0],
|
||
|
|
username: result.rows[0][1],
|
||
|
|
email: result.rows[0][2],
|
||
|
|
passwordHash: result.rows[0][3],
|
||
|
|
salt: result.rows[0][4],
|
||
|
|
role: result.rows[0][5],
|
||
|
|
isActive: result.rows[0][6]
|
||
|
|
};
|
||
|
|
|
||
|
|
// 비밀번호 검증
|
||
|
|
const hashedPassword = crypto.pbkdf2Sync(password, user.salt, 10000, 64, 'sha512').toString('hex');
|
||
|
|
|
||
|
|
if (hashedPassword === user.passwordHash) {
|
||
|
|
delete user.passwordHash;
|
||
|
|
delete user.salt;
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('사용자 인증 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 키 생성
|
||
|
|
async function createApiKey(userId, keyName, permissions = []) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
// API 키 생성 (32바이트 랜덤)
|
||
|
|
const apiKey = 'ak_' + crypto.randomBytes(32).toString('hex');
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`INSERT INTO API_KEYS (USER_ID, KEY_NAME, API_KEY, PERMISSIONS, IS_ACTIVE)
|
||
|
|
VALUES (:userId, :keyName, :apiKey, :permissions, 1)`,
|
||
|
|
{
|
||
|
|
userId: userId,
|
||
|
|
keyName: keyName,
|
||
|
|
apiKey: apiKey,
|
||
|
|
permissions: JSON.stringify(permissions)
|
||
|
|
},
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
|
||
|
|
return { apiKey, rowsAffected: result.rowsAffected };
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 키 생성 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 키 검증
|
||
|
|
async function validateApiKey(apiKey) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`SELECT ak.ID, ak.USER_ID, ak.KEY_NAME, ak.PERMISSIONS, ak.USAGE_COUNT, ak.LAST_USED,
|
||
|
|
u.USERNAME, u.EMAIL, u.ROLE
|
||
|
|
FROM API_KEYS ak
|
||
|
|
JOIN USERS u ON ak.USER_ID = u.ID
|
||
|
|
WHERE ak.API_KEY = :apiKey AND ak.IS_ACTIVE = 1 AND u.IS_ACTIVE = 1`,
|
||
|
|
[apiKey]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const keyData = {
|
||
|
|
id: result.rows[0][0],
|
||
|
|
userId: result.rows[0][1],
|
||
|
|
keyName: result.rows[0][2],
|
||
|
|
permissions: JSON.parse(result.rows[0][3] || '[]'),
|
||
|
|
usageCount: result.rows[0][4],
|
||
|
|
lastUsed: result.rows[0][5],
|
||
|
|
user: {
|
||
|
|
username: result.rows[0][6],
|
||
|
|
email: result.rows[0][7],
|
||
|
|
role: result.rows[0][8]
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 사용 횟수 증가 및 마지막 사용 시간 업데이트
|
||
|
|
await connection.execute(
|
||
|
|
`UPDATE API_KEYS
|
||
|
|
SET USAGE_COUNT = USAGE_COUNT + 1, LAST_USED = CURRENT_TIMESTAMP
|
||
|
|
WHERE ID = :id`,
|
||
|
|
[keyData.id],
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
|
||
|
|
return keyData;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 키 검증 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 사용자의 API 키 목록 조회
|
||
|
|
async function getUserApiKeys(userId) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`SELECT ID, KEY_NAME, API_KEY, PERMISSIONS, USAGE_COUNT, LAST_USED, CREATED_AT, IS_ACTIVE
|
||
|
|
FROM API_KEYS
|
||
|
|
WHERE USER_ID = :userId
|
||
|
|
ORDER BY CREATED_AT DESC`,
|
||
|
|
[userId]
|
||
|
|
);
|
||
|
|
|
||
|
|
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]
|
||
|
|
}));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 키 목록 조회 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 키 비활성화
|
||
|
|
async function deactivateApiKey(keyId, userId) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`UPDATE API_KEYS
|
||
|
|
SET IS_ACTIVE = 0
|
||
|
|
WHERE ID = :keyId AND USER_ID = :userId`,
|
||
|
|
[keyId, userId],
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
|
||
|
|
return result.rowsAffected;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 키 비활성화 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 모든 사용자 조회 (관리자용)
|
||
|
|
async function getAllUsers() {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`SELECT ID, USERNAME, EMAIL, ROLE, IS_ACTIVE, CREATED_AT, LAST_LOGIN
|
||
|
|
FROM USERS
|
||
|
|
ORDER BY CREATED_AT DESC`
|
||
|
|
);
|
||
|
|
|
||
|
|
return result.rows.map(row => ({
|
||
|
|
id: row[0],
|
||
|
|
username: row[1],
|
||
|
|
email: row[2],
|
||
|
|
role: row[3],
|
||
|
|
isActive: row[4],
|
||
|
|
createdAt: row[5],
|
||
|
|
lastLogin: row[6]
|
||
|
|
}));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('사용자 목록 조회 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 사용 통계 조회
|
||
|
|
async function getApiUsageStats() {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`SELECT
|
||
|
|
COUNT(DISTINCT ak.USER_ID) as total_users,
|
||
|
|
COUNT(ak.ID) as total_keys,
|
||
|
|
SUM(ak.USAGE_COUNT) as total_requests,
|
||
|
|
COUNT(CASE WHEN ak.LAST_USED >= SYSDATE - 1 THEN 1 END) as active_keys_today
|
||
|
|
FROM API_KEYS ak
|
||
|
|
WHERE ak.IS_ACTIVE = 1`
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
totalUsers: result.rows[0][0],
|
||
|
|
totalKeys: result.rows[0][1],
|
||
|
|
totalRequests: result.rows[0][2],
|
||
|
|
activeKeysToday: result.rows[0][3]
|
||
|
|
};
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 사용 통계 조회 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 테이블 생성
|
||
|
|
async function createAuthTables() {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
// 사용자 테이블 생성
|
||
|
|
const checkUsersTable = await connection.execute(
|
||
|
|
`SELECT COUNT(*) as count FROM user_tables WHERE table_name = 'USERS'`
|
||
|
|
);
|
||
|
|
|
||
|
|
if (checkUsersTable.rows[0][0] === 0) {
|
||
|
|
await connection.execute(
|
||
|
|
`CREATE TABLE USERS (
|
||
|
|
ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
|
|
USERNAME VARCHAR2(50) UNIQUE NOT NULL,
|
||
|
|
EMAIL VARCHAR2(100) UNIQUE NOT NULL,
|
||
|
|
PASSWORD_HASH VARCHAR2(128) NOT NULL,
|
||
|
|
SALT VARCHAR2(64) NOT NULL,
|
||
|
|
ROLE VARCHAR2(20) DEFAULT 'user' CHECK (ROLE IN ('admin', 'user')),
|
||
|
|
IS_ACTIVE NUMBER(1) DEFAULT 1,
|
||
|
|
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
LAST_LOGIN TIMESTAMP
|
||
|
|
)`,
|
||
|
|
[],
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
console.log('USERS 테이블이 생성되었습니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 키 테이블 생성
|
||
|
|
const checkApiKeysTable = await connection.execute(
|
||
|
|
`SELECT COUNT(*) as count FROM user_tables WHERE table_name = 'API_KEYS'`
|
||
|
|
);
|
||
|
|
|
||
|
|
if (checkApiKeysTable.rows[0][0] === 0) {
|
||
|
|
await connection.execute(
|
||
|
|
`CREATE TABLE API_KEYS (
|
||
|
|
ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||
|
|
USER_ID NUMBER NOT NULL,
|
||
|
|
KEY_NAME VARCHAR2(100) NOT NULL,
|
||
|
|
API_KEY VARCHAR2(100) UNIQUE NOT NULL,
|
||
|
|
PERMISSIONS CLOB,
|
||
|
|
USAGE_COUNT NUMBER DEFAULT 0,
|
||
|
|
IS_ACTIVE NUMBER(1) DEFAULT 1,
|
||
|
|
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
LAST_USED TIMESTAMP,
|
||
|
|
CONSTRAINT FK_API_KEYS_USER FOREIGN KEY (USER_ID) REFERENCES USERS(ID)
|
||
|
|
)`,
|
||
|
|
[],
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
console.log('API_KEYS 테이블이 생성되었습니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 인덱스 생성
|
||
|
|
try {
|
||
|
|
await connection.execute(`CREATE INDEX IDX_API_KEYS_KEY ON API_KEYS(API_KEY)`);
|
||
|
|
await connection.execute(`CREATE INDEX IDX_API_KEYS_USER ON API_KEYS(USER_ID)`);
|
||
|
|
await connection.execute(`CREATE INDEX IDX_USERS_USERNAME ON USERS(USERNAME)`);
|
||
|
|
} catch (err) {
|
||
|
|
// 인덱스가 이미 존재할 수 있음
|
||
|
|
}
|
||
|
|
|
||
|
|
// 기본 관리자 계정 생성
|
||
|
|
const checkAdmin = await connection.execute(
|
||
|
|
`SELECT COUNT(*) as count FROM USERS WHERE USERNAME = 'admin'`
|
||
|
|
);
|
||
|
|
|
||
|
|
if (checkAdmin.rows[0][0] === 0) {
|
||
|
|
const salt = crypto.randomBytes(32).toString('hex');
|
||
|
|
const hashedPassword = crypto.pbkdf2Sync('admin123!', salt, 10000, 64, 'sha512').toString('hex');
|
||
|
|
|
||
|
|
await connection.execute(
|
||
|
|
`INSERT INTO USERS (USERNAME, EMAIL, PASSWORD_HASH, SALT, ROLE)
|
||
|
|
VALUES ('admin', 'admin@restapi.com', :passwordHash, :salt, 'admin')`,
|
||
|
|
{
|
||
|
|
passwordHash: hashedPassword,
|
||
|
|
salt: salt
|
||
|
|
},
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
console.log('기본 관리자 계정이 생성되었습니다. (admin/admin123!)');
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch (err) {
|
||
|
|
console.error('인증 테이블 생성 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// API 키 삭제
|
||
|
|
async function deleteApiKey(keyId, userId) {
|
||
|
|
let connection;
|
||
|
|
try {
|
||
|
|
connection = await getConnection();
|
||
|
|
|
||
|
|
const result = await connection.execute(
|
||
|
|
`DELETE FROM API_KEYS
|
||
|
|
WHERE id = :keyId AND user_id = :userId`,
|
||
|
|
{ keyId, userId },
|
||
|
|
{ autoCommit: true }
|
||
|
|
);
|
||
|
|
|
||
|
|
return result.rowsAffected;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('API 키 삭제 실패:', err);
|
||
|
|
throw err;
|
||
|
|
} finally {
|
||
|
|
if (connection) {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
createUser,
|
||
|
|
authenticateUser,
|
||
|
|
createApiKey,
|
||
|
|
validateApiKey,
|
||
|
|
getUserApiKeys,
|
||
|
|
deactivateApiKey,
|
||
|
|
deleteApiKey,
|
||
|
|
getAllUsers,
|
||
|
|
getApiUsageStats,
|
||
|
|
createAuthTables
|
||
|
|
};
|