Files
tk-factory-services/sso-auth-service/controllers/authController.js
Hyungi Ahn 7637be33f3 feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합
TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가

작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영

권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)

기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:46:21 +09:00

362 lines
9.6 KiB
JavaScript

/**
* 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,
worker_id: user.worker_id || null,
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,
worker_id: user.worker_id || null,
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,
worker_id: user.worker_id || null,
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
};