Crawl Manager
+관리자 로그인
+diff --git a/package.json b/package.json index 1c7af53..3b072d9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.7.0", "ejs": "^3.1.10", "dotenv": "^16.4.0", - "https-proxy-agent": "^7.0.0" + "https-proxy-agent": "^7.0.0", + "cookie-parser": "^1.4.6" } } diff --git a/src/app.js b/src/app.js index d250b91..0017eda 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,8 @@ require('dotenv').config(); +const crypto = require('crypto'); const express = require('express'); +const cookieParser = require('cookie-parser'); const path = require('path'); const db = require('./db'); const apiRouter = require('./routes/api'); @@ -10,9 +12,15 @@ const { initScheduler } = require('./services/scheduler'); const app = express(); const PORT = process.env.PORT || 3000; +// 세션 토큰 저장소 (메모리 - 재시작 시 로그아웃됨) +const sessions = new Map(); +const SESSION_COOKIE = 'cm_session'; +const SESSION_MAX_AGE = 24 * 60 * 60 * 1000; // 24시간 + // ===== 미들웨어 ===== app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); // 정적 파일 app.use('/public', express.static(path.join(__dirname, '..', 'public'))); @@ -24,23 +32,62 @@ app.set('views', path.join(__dirname, '..', 'views')); // ===== 도메인 기반 라우팅 (최우선) ===== app.use(domainRouter); -// ===== Basic Auth (관리자 영역만) ===== +// ===== 인증 미들웨어 ===== function adminAuth(req, res, next) { - const authHeader = req.headers.authorization; - if (!authHeader) { - res.setHeader('WWW-Authenticate', 'Basic realm="Crawl Manager Admin"'); - return res.status(401).send('인증이 필요합니다'); + const token = req.cookies[SESSION_COOKIE]; + if (token && sessions.has(token)) { + const session = sessions.get(token); + if (Date.now() < session.expires) { + return next(); + } + sessions.delete(token); } - - const [user, pass] = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':'); - if (user === process.env.ADMIN_USER && pass === process.env.ADMIN_PASS) { - return next(); - } - - res.setHeader('WWW-Authenticate', 'Basic realm="Crawl Manager Admin"'); - res.status(401).send('인증 실패'); + // 로그인 페이지로 리다이렉트 + const returnUrl = req.originalUrl; + res.redirect('/login?redirect=' + encodeURIComponent(returnUrl)); } +// ===== 로그인 / 로그아웃 ===== +app.get('/login', (req, res) => { + // 이미 로그인된 상태면 관리자로 이동 + const token = req.cookies[SESSION_COOKIE]; + if (token && sessions.has(token) && Date.now() < sessions.get(token).expires) { + return res.redirect('/admin'); + } + res.render('admin/login', { error: null, redirect: req.query.redirect || '/admin' }); +}); + +app.post('/login', (req, res) => { + const { email, password, redirect } = req.body; + const redirectUrl = redirect || '/admin'; + + if (email === process.env.ADMIN_USER && password === process.env.ADMIN_PASS) { + const token = crypto.randomBytes(32).toString('hex'); + sessions.set(token, { + user: email, + expires: Date.now() + SESSION_MAX_AGE, + }); + + res.cookie(SESSION_COOKIE, token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: SESSION_MAX_AGE, + }); + + return res.redirect(redirectUrl); + } + + res.render('admin/login', { error: '이메일 또는 비밀번호가 올바르지 않습니다.', redirect: redirectUrl }); +}); + +app.get('/logout', (req, res) => { + const token = req.cookies[SESSION_COOKIE]; + if (token) sessions.delete(token); + res.clearCookie(SESSION_COOKIE); + res.redirect('/login'); +}); + // ===== 루트 → 관리자로 리다이렉트 ===== app.get('/', (req, res) => res.redirect('/admin')); @@ -78,11 +125,9 @@ app.use('/', publicRouter); // ===== 시작 ===== async function start() { try { - // DB 연결 대기 (재시도) await db.waitForDB(); console.log('[DB] PostgreSQL 연결 성공'); - // 스케줄러 초기화 await initScheduler(); app.listen(PORT, '0.0.0.0', () => { diff --git a/views/admin/layout.ejs b/views/admin/layout.ejs index f0f8748..ac59c4c 100644 --- a/views/admin/layout.ejs +++ b/views/admin/layout.ejs @@ -101,12 +101,18 @@ tr:hover td{background:rgba(255,255,255,.02)} 💰 AdSense 관리 🔗 도메인 매핑 📝 로그 +
관리자 로그인
+