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:
18
sso-auth-service/Dockerfile
Normal file
18
sso-auth-service/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN chown -R node:node /usr/src/app
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
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
|
||||
};
|
||||
49
sso-auth-service/index.js
Normal file
49
sso-auth-service/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* SSO Auth Service - 중앙 인증 서비스
|
||||
*
|
||||
* TK Factory Services의 통합 인증을 담당
|
||||
* - JWT 발급/검증/갱신
|
||||
* - 사용자 CRUD
|
||||
* - bcrypt + pbkdf2 비밀번호 호환
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', service: 'sso-auth', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Auth routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ success: false, error: 'Not Found' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('SSO Auth Error:', err.message);
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
error: err.message || 'Internal Server Error'
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`SSO Auth Service running on port ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
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
|
||||
};
|
||||
18
sso-auth-service/package.json
Normal file
18
sso-auth-service/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "sso-auth-service",
|
||||
"version": "1.0.0",
|
||||
"description": "TK Factory Services - 중앙 SSO 인증 서비스",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node --watch index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"redis": "^5.9.0"
|
||||
}
|
||||
}
|
||||
42
sso-auth-service/routes/authRoutes.js
Normal file
42
sso-auth-service/routes/authRoutes.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* SSO Auth Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/authController');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Middleware: admin 체크
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = req.headers['authorization']?.split(' ')[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.SSO_JWT_SECRET);
|
||||
if (!['admin', 'system'].includes(decoded.role)) {
|
||||
return res.status(403).json({ success: false, error: '관리자 권한이 필요합니다' });
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
// 공개 엔드포인트
|
||||
router.post('/login', authController.login);
|
||||
router.post('/login/form', express.urlencoded({ extended: true }), authController.loginForm);
|
||||
router.get('/validate', authController.validate);
|
||||
router.get('/me', authController.me);
|
||||
router.post('/refresh', authController.refresh);
|
||||
router.post('/logout', authController.logout);
|
||||
|
||||
// 관리자 엔드포인트
|
||||
router.get('/users', requireAdmin, authController.getUsers);
|
||||
router.post('/users', requireAdmin, authController.createUser);
|
||||
router.put('/users/:id', requireAdmin, authController.updateUser);
|
||||
router.delete('/users/:id', requireAdmin, authController.deleteUser);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user