/** * SSO Auth Controller * * 로그인, 토큰 검증/갱신, 사용자 CRUD */ const jwt = require('jsonwebtoken'); const userModel = require('../models/userModel'); const redis = require('../utils/redis'); 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'; const MAX_LOGIN_ATTEMPTS = 5; const LOGIN_LOCKOUT_SECONDS = 300; // 5분 /** * JWT 토큰 페이로드 생성 (모든 시스템 공통 구조) */ function createTokenPayload(user) { return { user_id: user.user_id, id: user.user_id, username: user.username, name: user.name, department_id: user.department_id || null, department_name: user.department_name || user.department || null, is_production: user.is_production || false, 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 attemptKey = `login_attempts:${username}`; const attempts = parseInt(await redis.get(attemptKey)) || 0; if (attempts >= MAX_LOGIN_ATTEMPTS) { return res.status(429).json({ success: false, error: '로그인 시도 횟수를 초과했습니다. 5분 후 다시 시도하세요' }); } const user = await userModel.findByUsername(username); if (!user) { await redis.incr(attemptKey); await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS); return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' }); } const valid = await userModel.verifyPassword(password, user.password_hash); if (!valid) { await redis.incr(attemptKey); await redis.expire(attemptKey, LOGIN_LOCKOUT_SECONDS); return res.status(401).json({ success: false, error: '사용자명 또는 비밀번호가 올바르지 않습니다' }); } // 로그인 성공 시 시도 횟수 초기화 await redis.del(attemptKey); 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 쿠키는 클라이언트(login.html)에서 domain=.technicalkorea.net으로 설정 // 서버 httpOnly 쿠키는 서브도메인 공유 불가하므로 제거 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, department_id: user.department_id || null, department_name: user.department_name || user.department || null, is_production: user.is_production || false, 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, department_id: user.department_id || null, department_name: user.department_name || user.department || null, is_production: user.is_production || false, 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.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 (SSO 토큰은 항상 Bearer로 전달) const authHeader = req.headers['authorization']; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.split(' ')[1]; } return null; } module.exports = { login, loginForm, validate, me, refresh, logout, getUsers, createUser, updateUser, deleteUser };