Files
tk-factory-services/tksupport/api/controllers/vacationController.js
Hyungi Ahn 12367dd3a1 fix(security): 전체 서비스 보안 점검 — XSS·인가·토큰·헤더·에러마스킹 일괄 수정
Phase 1 CRITICAL XSS:
- marked.parse() → DOMPurify.sanitize() (system3 ai-assistant, issues-management)
- toast innerHTML에 escapeHtml 적용 (system1 api-base, system3 common-header)
- onclick 핸들러 → data 속성 + addEventListener (system2 issue-detail)

Phase 2 HIGH 인가:
- getUserBalance 본인확인 추가 (tksupport vacationController)

Phase 3 HIGH 토큰+CSP:
- localStorage 토큰 저장 제거 — 쿠키 전용 (7개 서비스)
- unsafe-eval CSP 제거 (system1 security.js)

Phase 4 MEDIUM:
- nginx 보안 헤더 추가 (8개 서비스)
- 500 에러 메시지 마스킹 (5개 API)
- path traversal 방지 (system3 file_service.py)
- cookie fallback 데드코드 제거 (4개 auth.js)
- /login/form rate limiting 추가 (sso-auth)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:50:00 +09:00

326 lines
13 KiB
JavaScript

const vacationRequestModel = require('../models/vacationRequestModel');
const vacationBalanceModel = require('../models/vacationBalanceModel');
const { getPool } = require('../middleware/auth');
const vacationController = {
// ─── 휴가 신청 ───
async createRequest(req, res) {
try {
const { vacation_type_id, start_date, end_date, days_used, reason } = req.body;
const user_id = req.user.user_id || req.user.id;
if (!vacation_type_id || !start_date || !end_date || !days_used) {
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
}
if (new Date(end_date) < new Date(start_date)) {
return res.status(400).json({ success: false, error: '종료일은 시작일보다 이후여야 합니다' });
}
const overlapRows = await vacationRequestModel.checkOverlap(user_id, start_date, end_date);
if (overlapRows[0].count > 0) {
return res.status(400).json({ success: false, error: '해당 기간에 이미 신청된 휴가가 있습니다' });
}
const result = await vacationRequestModel.create({
user_id, vacation_type_id, start_date, end_date,
days_used, reason: reason || null, status: 'pending'
});
res.status(201).json({
success: true,
message: '휴가 신청이 완료되었습니다',
data: { request_id: result.insertId }
});
} catch (error) {
console.error('휴가 신청 생성 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async getRequests(req, res) {
try {
const role = (req.user.role || '').toLowerCase();
const userId = req.user.user_id || req.user.id;
const filters = {
status: req.query.status,
start_date: req.query.start_date,
end_date: req.query.end_date,
vacation_type_id: req.query.vacation_type_id
};
if (!['admin', 'system'].includes(role)) {
filters.user_id = userId;
} else if (req.query.user_id) {
filters.user_id = req.query.user_id;
}
const results = await vacationRequestModel.getAll(filters);
res.json({ success: true, data: results });
} catch (error) {
console.error('휴가 신청 목록 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async getRequestById(req, res) {
try {
const results = await vacationRequestModel.getById(req.params.id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
}
const request = results[0];
const role = (req.user.role || '').toLowerCase();
const userId = req.user.user_id || req.user.id;
if (!['admin', 'system'].includes(role) && userId !== request.user_id) {
return res.status(403).json({ success: false, error: '권한이 없습니다' });
}
res.json({ success: true, data: request });
} catch (error) {
console.error('휴가 신청 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async updateRequest(req, res) {
try {
const { id } = req.params;
const { start_date, end_date, days_used, reason, vacation_type_id } = req.body;
const results = await vacationRequestModel.getById(id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
}
const existing = results[0];
const role = (req.user.role || '').toLowerCase();
const userId = req.user.user_id || req.user.id;
if (!['admin', 'system'].includes(role) && userId !== existing.user_id) {
return res.status(403).json({ success: false, error: '권한이 없습니다' });
}
if (existing.status !== 'pending') {
return res.status(400).json({ success: false, error: '대기 중인 신청만 수정할 수 있습니다' });
}
const updateData = {};
if (vacation_type_id) updateData.vacation_type_id = vacation_type_id;
if (start_date) updateData.start_date = start_date;
if (end_date) updateData.end_date = end_date;
if (days_used) updateData.days_used = days_used;
if (reason !== undefined) updateData.reason = reason;
if (start_date || end_date) {
const newStart = start_date || existing.start_date;
const newEnd = end_date || existing.end_date;
const overlapRows = await vacationRequestModel.checkOverlap(existing.user_id, newStart, newEnd, id);
if (overlapRows[0].count > 0) {
return res.status(400).json({ success: false, error: '해당 기간에 이미 신청된 휴가가 있습니다' });
}
}
await vacationRequestModel.update(id, updateData);
res.json({ success: true, message: '휴가 신청이 수정되었습니다' });
} catch (error) {
console.error('휴가 신청 수정 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async cancelRequest(req, res) {
try {
const { id } = req.params;
const results = await vacationRequestModel.getById(id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
}
const existing = results[0];
const role = (req.user.role || '').toLowerCase();
const userId = req.user.user_id || req.user.id;
if (!['admin', 'system'].includes(role) && userId !== existing.user_id) {
return res.status(403).json({ success: false, error: '권한이 없습니다' });
}
if (existing.status === 'cancelled') {
return res.status(400).json({ success: false, error: '이미 취소된 신청입니다' });
}
// 승인된 건 취소 시 잔여일 복구
if (existing.status === 'approved') {
const year = new Date(existing.start_date).getFullYear();
await vacationBalanceModel.restoreDays(
existing.user_id, existing.vacation_type_id, year, parseFloat(existing.days_used)
);
}
await vacationRequestModel.updateStatus(id, {
status: 'cancelled',
reviewed_by: userId,
review_note: '취소됨'
});
res.json({ success: true, message: '휴가 신청이 취소되었습니다' });
} catch (error) {
console.error('휴가 취소 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
// ─── 승인/반려 (관리자) ───
async getPending(req, res) {
try {
const results = await vacationRequestModel.getAllPending();
res.json({ success: true, data: results });
} catch (error) {
console.error('대기 목록 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async approveRequest(req, res) {
try {
const { id } = req.params;
const { review_note } = req.body;
const reviewed_by = req.user.user_id || req.user.id;
const results = await vacationRequestModel.getById(id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
}
if (results[0].status !== 'pending') {
return res.status(400).json({ success: false, error: '이미 처리된 신청입니다' });
}
const request = results[0];
// 잔여일 차감
const year = new Date(request.start_date).getFullYear();
await vacationBalanceModel.deductDays(
request.user_id, request.vacation_type_id, year, parseFloat(request.days_used)
);
await vacationRequestModel.updateStatus(id, { status: 'approved', reviewed_by, review_note });
res.json({ success: true, message: '휴가 신청이 승인되었습니다' });
} catch (error) {
console.error('휴가 승인 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async rejectRequest(req, res) {
try {
const { id } = req.params;
const { review_note } = req.body;
const reviewed_by = req.user.user_id || req.user.id;
const results = await vacationRequestModel.getById(id);
if (results.length === 0) {
return res.status(404).json({ success: false, error: '해당 휴가 신청을 찾을 수 없습니다' });
}
if (results[0].status !== 'pending') {
return res.status(400).json({ success: false, error: '이미 처리된 신청입니다' });
}
await vacationRequestModel.updateStatus(id, { status: 'rejected', reviewed_by, review_note });
res.json({ success: true, message: '휴가 신청이 반려되었습니다' });
} catch (error) {
console.error('휴가 반려 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
// ─── 잔여일 ───
async getMyBalance(req, res) {
try {
const userId = req.user.user_id || req.user.id;
const year = parseInt(req.query.year) || new Date().getFullYear();
const balances = await vacationBalanceModel.getByUserAndYear(userId, year);
const hireDate = await vacationBalanceModel.getUserHireDate(userId);
res.json({ success: true, data: { balances, hire_date: hireDate } });
} catch (error) {
console.error('잔여일 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async getUserBalance(req, res) {
try {
const { userId } = req.params;
const requestedId = parseInt(userId);
const currentUserId = req.user.user_id || req.user.id;
const role = (req.user.role || '').toLowerCase();
if (requestedId !== currentUserId && !['admin', 'system'].includes(role)) {
return res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
}
const year = parseInt(req.query.year) || new Date().getFullYear();
const balances = await vacationBalanceModel.getByUserAndYear(userId, year);
const hireDate = await vacationBalanceModel.getUserHireDate(userId);
res.json({ success: true, data: { balances, hire_date: hireDate } });
} catch (error) {
console.error('사용자 잔여일 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async allocateBalance(req, res) {
try {
const { user_id, vacation_type_id, year, total_days, notes } = req.body;
const created_by = req.user.user_id || req.user.id;
if (!user_id || !vacation_type_id || !year || total_days === undefined) {
return res.status(400).json({ success: false, error: '필수 필드가 누락되었습니다' });
}
await vacationBalanceModel.allocate({ user_id, vacation_type_id, year, total_days, notes, created_by });
res.json({ success: true, message: '휴가 잔여일이 배정되었습니다' });
} catch (error) {
console.error('잔여일 배정 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
async getAllBalances(req, res) {
try {
const year = parseInt(req.query.year) || new Date().getFullYear();
const balances = await vacationBalanceModel.getAllByYear(year);
res.json({ success: true, data: balances });
} catch (error) {
console.error('전체 잔여일 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
// ─── 참조 데이터 ───
async getVacationTypes(req, res) {
try {
const db = getPool();
const [rows] = await db.query('SELECT * FROM vacation_types ORDER BY priority ASC, type_name ASC');
res.json({ success: true, data: rows });
} catch (error) {
console.error('휴가 유형 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
},
// ─── 사용자 목록 (관리자 - 배정용) ───
async getUsers(req, res) {
try {
const db = getPool();
const [rows] = await db.query(`
SELECT user_id, username, name, hire_date
FROM sso_users
WHERE is_active = 1
ORDER BY name ASC
`);
res.json({ success: true, data: rows });
} catch (error) {
console.error('사용자 목록 조회 오류:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
}
}
};
module.exports = vacationController;