From c4d148810e88eb2dd05d443e1109ccae1bcaabbf Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 22 Dec 2025 15:37:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BD=98=EC=86=94=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=8B=A4=ED=96=89=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드: admin.js 라우트 (JWT + 관리자 권한) - 프론트엔드: 실제 API 호출로 연동 --- backend/routes/admin.js | 98 ++++++++++++++++++++++++++++++++++++ backend/server.js | 4 ++ frontend/src/pages/Admin.jsx | 51 ++++++++++++++++--- 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 backend/routes/admin.js diff --git a/backend/routes/admin.js b/backend/routes/admin.js new file mode 100644 index 0000000..fcac07a --- /dev/null +++ b/backend/routes/admin.js @@ -0,0 +1,98 @@ +/** + * 관리자 전용 API 라우트 + * - 서버 명령어 실행 + * - 인증 + 관리자 권한 필요 + */ + +import express from "express"; +import jwt from "jsonwebtoken"; +import { pool } from "../lib/db.js"; + +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET || "minecraft-jwt-secret"; +const MOD_API_URL = `http://${ + process.env.MINECRAFT_HOST || "host.docker.internal" +}:${process.env.MINECRAFT_MOD_PORT || 25580}`; + +/** + * JWT 토큰에서 사용자 정보 추출 + */ +function getUserFromToken(req) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return null; + } + + try { + const token = authHeader.split(" ")[1]; + return jwt.verify(token, JWT_SECRET); + } catch { + return null; + } +} + +/** + * 관리자 권한 확인 미들웨어 + */ +async function requireAdmin(req, res, next) { + const user = getUserFromToken(req); + + if (!user) { + return res.status(401).json({ error: "인증이 필요합니다" }); + } + + try { + // DB에서 관리자 권한 확인 + const result = await pool.query( + "SELECT is_admin FROM users WHERE id = $1 AND is_active = true", + [user.id] + ); + + if (result.rows.length === 0 || !result.rows[0].is_admin) { + return res.status(403).json({ error: "관리자 권한이 필요합니다" }); + } + + req.user = user; + next(); + } catch (error) { + console.error("[Admin] 권한 확인 오류:", error); + res.status(500).json({ error: "서버 오류" }); + } +} + +// 모든 라우트에 관리자 권한 필요 +router.use(requireAdmin); + +/** + * POST /admin/command - 서버 명령어 실행 + */ +router.post("/command", async (req, res) => { + const { command } = req.body; + + if (!command || typeof command !== "string" || !command.trim()) { + return res + .status(400) + .json({ success: false, message: "명령어를 입력해주세요" }); + } + + try { + console.log(`[Admin] ${req.user.email}님이 명령어 실행: ${command}`); + + const response = await fetch(`${MOD_API_URL}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ command: command.trim() }), + }); + + const data = await response.json(); + res.json(data); + } catch (error) { + console.error("[Admin] 명령어 전송 오류:", error); + res + .status(500) + .json({ success: false, message: "서버에 연결할 수 없습니다" }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 4aba6c6..80601ee 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,6 +17,7 @@ import { import apiRoutes from "./routes/api.js"; import authRoutes from "./routes/auth.js"; import linkRoutes from "./routes/link.js"; +import adminRoutes from "./routes/admin.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -66,6 +67,9 @@ app.use("/auth", authRoutes); // 마인크래프트 연동 라우트 app.use("/link", linkRoutes); +// 관리자 라우트 +app.use("/admin", adminRoutes); + // Socket.IO 연결 처리 io.on("connection", (socket) => { console.log("클라이언트 연결됨:", socket.id); diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 306b895..bc9b9e9 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -104,18 +104,53 @@ export default function Admin({ isMobile = false }) { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); - // 명령어 실행 (더미) - const handleCommand = () => { + // 명령어 실행 (실제 API 호출) + const handleCommand = async () => { if (!command.trim()) return; - const newLogs = [ - { time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), type: 'command', message: `> ${command}` }, - { time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), type: 'info', message: `[Server] 명령어 실행됨: ${command}` } - ]; + const timestamp = new Date().toLocaleTimeString('ko-KR', { hour12: false }); + + // 입력한 명령어를 로그에 추가 + setLogs(prev => [...prev, { time: timestamp, type: 'command', message: `> ${command}` }]); + + try { + const token = localStorage.getItem('token'); + const response = await fetch('/admin/command', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ command: command.trim() }) + }); + + const data = await response.json(); + + if (data.success) { + setLogs(prev => [...prev, { + time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), + type: 'info', + message: `[Server] ${data.message}` + }]); + setToast('명령어가 실행되었습니다.'); + } else { + setLogs(prev => [...prev, { + time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), + type: 'error', + message: `[Error] ${data.message || '명령어 실행 실패'}` + }]); + setToast(data.message || '명령어 실행 실패'); + } + } catch (error) { + setLogs(prev => [...prev, { + time: new Date().toLocaleTimeString('ko-KR', { hour12: false }), + type: 'error', + message: `[Error] 서버에 연결할 수 없습니다` + }]); + setToast('서버에 연결할 수 없습니다'); + } - setLogs(prev => [...prev, ...newLogs]); setCommand(''); - setToast('명령어가 실행되었습니다.'); }; // 플레이어 액션 핸들러