feat: 3-System 분리 프로젝트 초기 코드 작성
TK-FB(공장관리+신고)와 M-Project(부적합관리)를 3개 독립 시스템으로 분리하기 위한 전체 코드 구조 작성. - SSO 인증 서비스 (bcrypt + pbkdf2 이중 해시 지원) - System 1: 공장관리 (TK-FB 기반, 신고 코드 제거) - System 2: 신고 (TK-FB에서 workIssue 코드 추출) - System 3: 부적합관리 (M-Project 기반) - Gateway 포털 (path-based 라우팅) - 통합 docker-compose.yml 및 배포 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
172
sso-auth-service/models/userModel.js
Normal file
172
sso-auth-service/models/userModel.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* SSO User Model
|
||||
*
|
||||
* sso_users 테이블 CRUD 및 비밀번호 검증
|
||||
* bcrypt + pbkdf2_sha256 둘 다 지원
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let pool;
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'mariadb',
|
||||
port: parseInt(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'hyungi_user',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'hyungi',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* pbkdf2_sha256 해시 검증 (M-Project/passlib 호환)
|
||||
* passlib 형식: $pbkdf2-sha256$rounds$salt$hash
|
||||
*/
|
||||
function verifyPbkdf2(password, storedHash) {
|
||||
try {
|
||||
const parts = storedHash.split('$');
|
||||
if (parts.length < 5) return false;
|
||||
|
||||
const rounds = parseInt(parts[2]);
|
||||
// passlib uses adapted base64 (. instead of +, no padding)
|
||||
const salt = parts[3].replace(/\./g, '+');
|
||||
const hash = parts[4].replace(/\./g, '+');
|
||||
|
||||
// Pad base64 if needed
|
||||
const padded = (s) => s + '='.repeat((4 - s.length % 4) % 4);
|
||||
|
||||
const saltBuffer = Buffer.from(padded(salt), 'base64');
|
||||
const expectedHash = Buffer.from(padded(hash), 'base64');
|
||||
|
||||
const derivedKey = crypto.pbkdf2Sync(password, saltBuffer, rounds, expectedHash.length, 'sha256');
|
||||
return crypto.timingSafeEqual(derivedKey, expectedHash);
|
||||
} catch (err) {
|
||||
console.error('pbkdf2 verify error:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 검증 (bcrypt 또는 pbkdf2_sha256 자동 감지)
|
||||
*/
|
||||
async function verifyPassword(password, storedHash) {
|
||||
if (!password || !storedHash) return false;
|
||||
|
||||
// pbkdf2-sha256 형식 (passlib)
|
||||
if (storedHash.startsWith('$pbkdf2-sha256$')) {
|
||||
return verifyPbkdf2(password, storedHash);
|
||||
}
|
||||
|
||||
// bcrypt 형식
|
||||
if (storedHash.startsWith('$2b$') || storedHash.startsWith('$2a$')) {
|
||||
return bcrypt.compare(password, storedHash);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* bcrypt로 비밀번호 해시 생성 (새 비밀번호는 항상 bcrypt)
|
||||
*/
|
||||
async function hashPassword(password) {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
async function findByUsername(username) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE username = ? AND is_active = TRUE',
|
||||
[username]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function findById(userId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function findAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT user_id, username, name, department, role, system1_access, system2_access, system3_access, is_active, last_login, created_at FROM sso_users ORDER BY user_id'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function create({ username, password, name, department, role }) {
|
||||
const db = getPool();
|
||||
const password_hash = await hashPassword(password);
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO sso_users (username, password_hash, name, department, role)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[username, password_hash, name || null, department || null, role || 'user']
|
||||
);
|
||||
return findById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(userId, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
|
||||
if (data.department !== undefined) { fields.push('department = ?'); values.push(data.department); }
|
||||
if (data.role !== undefined) { fields.push('role = ?'); values.push(data.role); }
|
||||
if (data.system1_access !== undefined) { fields.push('system1_access = ?'); values.push(data.system1_access); }
|
||||
if (data.system2_access !== undefined) { fields.push('system2_access = ?'); values.push(data.system2_access); }
|
||||
if (data.system3_access !== undefined) { fields.push('system3_access = ?'); values.push(data.system3_access); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (data.password) {
|
||||
fields.push('password_hash = ?');
|
||||
values.push(await hashPassword(data.password));
|
||||
}
|
||||
|
||||
if (fields.length === 0) return findById(userId);
|
||||
|
||||
values.push(userId);
|
||||
await db.query(
|
||||
`UPDATE sso_users SET ${fields.join(', ')} WHERE user_id = ?`,
|
||||
values
|
||||
);
|
||||
return findById(userId);
|
||||
}
|
||||
|
||||
async function updateLastLogin(userId) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE sso_users SET last_login = NOW() WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteUser(userId) {
|
||||
const db = getPool();
|
||||
await db.query('UPDATE sso_users SET is_active = FALSE WHERE user_id = ?', [userId]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyPassword,
|
||||
hashPassword,
|
||||
findByUsername,
|
||||
findById,
|
||||
findAll,
|
||||
create,
|
||||
update,
|
||||
updateLastLogin,
|
||||
deleteUser,
|
||||
getPool
|
||||
};
|
||||
Reference in New Issue
Block a user