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:
358
sso-auth-service/controllers/authController.js
Normal file
358
sso-auth-service/controllers/authController.js
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* SSO Auth Controller
|
||||
*
|
||||
* 로그인, 토큰 검증/갱신, 사용자 CRUD
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const userModel = require('../models/userModel');
|
||||
|
||||
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
||||
const JWT_EXPIRES_IN = process.env.SSO_JWT_EXPIRES_IN || '7d';
|
||||
const JWT_REFRESH_SECRET = process.env.SSO_JWT_REFRESH_SECRET;
|
||||
const JWT_REFRESH_EXPIRES_IN = process.env.SSO_JWT_REFRESH_EXPIRES_IN || '30d';
|
||||
|
||||
/**
|
||||
* JWT 토큰 페이로드 생성 (모든 시스템 공통 구조)
|
||||
*/
|
||||
function createTokenPayload(user) {
|
||||
return {
|
||||
user_id: user.user_id,
|
||||
id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
access_level: user.role,
|
||||
sub: user.username,
|
||||
system_access: {
|
||||
system1: user.system1_access,
|
||||
system2: user.system2_access,
|
||||
system3: user.system3_access
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
async function login(req, res, next) {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, error: '사용자명과 비밀번호를 입력하세요' });
|
||||
}
|
||||
|
||||
const user = await userModel.findByUsername(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
||||
}
|
||||
|
||||
const valid = await userModel.verifyPassword(password, user.password_hash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' });
|
||||
}
|
||||
|
||||
await userModel.updateLastLogin(user.user_id);
|
||||
|
||||
const payload = createTokenPayload(user);
|
||||
const access_token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
const refresh_token = jwt.sign(
|
||||
{ user_id: user.user_id, type: 'refresh' },
|
||||
JWT_REFRESH_SECRET,
|
||||
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
||||
);
|
||||
|
||||
// SSO 쿠키 설정 (동일 도메인 공유)
|
||||
res.cookie('sso_token', access_token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
path: '/'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
access_token,
|
||||
refresh_token,
|
||||
token_type: 'bearer',
|
||||
user: {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
system_access: payload.system_access
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/login/form - OAuth2 form data login (M-Project 호환)
|
||||
*/
|
||||
async function loginForm(req, res, next) {
|
||||
try {
|
||||
const username = req.body.username;
|
||||
const password = req.body.password;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ detail: 'Missing username or password' });
|
||||
}
|
||||
|
||||
const user = await userModel.findByUsername(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ detail: 'Incorrect username or password' });
|
||||
}
|
||||
|
||||
const valid = await userModel.verifyPassword(password, user.password_hash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ detail: 'Incorrect username or password' });
|
||||
}
|
||||
|
||||
await userModel.updateLastLogin(user.user_id);
|
||||
|
||||
const payload = createTokenPayload(user);
|
||||
const access_token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
|
||||
res.json({
|
||||
access_token,
|
||||
token_type: 'bearer',
|
||||
user: {
|
||||
id: user.user_id,
|
||||
username: user.username,
|
||||
full_name: user.name,
|
||||
role: user.role,
|
||||
department: user.department
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/validate
|
||||
*/
|
||||
async function validate(req, res, next) {
|
||||
try {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '토큰이 필요합니다' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const user = await userModel.findById(decoded.user_id || decoded.id);
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 사용자입니다' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
role: user.role,
|
||||
system_access: {
|
||||
system1: user.system1_access,
|
||||
system2: user.system2_access,
|
||||
system3: user.system3_access
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/me - 현재 사용자 정보 (M-Project 호환)
|
||||
*/
|
||||
async function me(req, res, next) {
|
||||
try {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ detail: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const user = await userModel.findById(decoded.user_id || decoded.id);
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({ detail: 'User not found or inactive' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: user.user_id,
|
||||
username: user.username,
|
||||
full_name: user.name,
|
||||
role: user.role,
|
||||
department: user.department,
|
||||
is_active: user.is_active
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({ detail: 'Could not validate credentials' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
*/
|
||||
async function refresh(req, res, next) {
|
||||
try {
|
||||
const { refresh_token } = req.body;
|
||||
if (!refresh_token) {
|
||||
return res.status(400).json({ success: false, error: 'Refresh 토큰이 필요합니다' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(refresh_token, JWT_REFRESH_SECRET);
|
||||
if (decoded.type !== 'refresh') {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 Refresh 토큰입니다' });
|
||||
}
|
||||
|
||||
const user = await userModel.findById(decoded.user_id);
|
||||
if (!user || !user.is_active) {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 사용자입니다' });
|
||||
}
|
||||
|
||||
const payload = createTokenPayload(user);
|
||||
const access_token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
const new_refresh_token = jwt.sign(
|
||||
{ user_id: user.user_id, type: 'refresh' },
|
||||
JWT_REFRESH_SECRET,
|
||||
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
|
||||
);
|
||||
|
||||
res.cookie('sso_token', access_token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
path: '/'
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
access_token,
|
||||
refresh_token: new_refresh_token,
|
||||
token_type: 'bearer'
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 Refresh 토큰입니다' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
async function logout(req, res) {
|
||||
res.clearCookie('sso_token', { path: '/' });
|
||||
res.json({ success: true, message: '로그아웃 되었습니다' });
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/users - 모든 사용자 목록
|
||||
*/
|
||||
async function getUsers(req, res, next) {
|
||||
try {
|
||||
const users = await userModel.findAll();
|
||||
res.json({ success: true, data: users });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/users - 사용자 생성
|
||||
*/
|
||||
async function createUser(req, res, next) {
|
||||
try {
|
||||
const { username, password, name, department, role } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, error: '사용자명과 비밀번호는 필수입니다' });
|
||||
}
|
||||
|
||||
const existing = await userModel.findByUsername(username);
|
||||
if (existing) {
|
||||
return res.status(409).json({ success: false, error: '이미 존재하는 사용자명입니다' });
|
||||
}
|
||||
|
||||
const user = await userModel.create({ username, password, name, department, role });
|
||||
res.status(201).json({ success: true, data: user });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/auth/users/:id - 사용자 수정
|
||||
*/
|
||||
async function updateUser(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const user = await userModel.update(userId, req.body);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: user });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/auth/users/:id - 사용자 비활성화
|
||||
*/
|
||||
async function deleteUser(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
await userModel.deleteUser(userId);
|
||||
res.json({ success: true, message: '사용자가 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bearer 토큰 또는 쿠키에서 토큰 추출
|
||||
*/
|
||||
function extractToken(req) {
|
||||
// Authorization header
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.split(' ')[1];
|
||||
}
|
||||
// Cookie
|
||||
if (req.cookies && req.cookies.sso_token) {
|
||||
return req.cookies.sso_token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
loginForm,
|
||||
validate,
|
||||
me,
|
||||
refresh,
|
||||
logout,
|
||||
getUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser
|
||||
};
|
||||
Reference in New Issue
Block a user