feat: 관리자 페이지 로그인 인증 추가
- server.js: 세션 토큰 기반 인증 미들웨어 추가 - POST /api/admin/login, POST /api/admin/logout, GET /api/admin/verify 엔드포인트 추가 - 관리자 전용 쓰기 라우트에 authMiddleware 적용 - admin.html: 로그인 화면 컴포넌트 및 토큰 관리(sessionStorage) 추가 - 사이드바에 로그아웃 버튼 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ const { randomUUID } = require('crypto');
|
||||
const PORT = 3000;
|
||||
const DB_PATH = path.join(__dirname, 'jaryo.db');
|
||||
const UPLOADS_DIR = path.join(__dirname, 'uploads');
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin1234';
|
||||
const sessions = new Set();
|
||||
|
||||
if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR);
|
||||
|
||||
@@ -186,6 +188,12 @@ function rowToFile(row) {
|
||||
return { ...row, starred: row.starred === 1 };
|
||||
}
|
||||
|
||||
function authMiddleware(req, res, next) {
|
||||
const token = (req.headers.authorization || '').replace('Bearer ', '').trim();
|
||||
if (!token || !sessions.has(token)) return res.status(401).json({ error: '인증이 필요합니다' });
|
||||
next();
|
||||
}
|
||||
|
||||
// ── Express ───────────────────────────────────────────────────
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
@@ -322,7 +330,7 @@ app.get('/api/files/:id/download', (req, res) => {
|
||||
});
|
||||
|
||||
// ── API: 삭제 ─────────────────────────────────────────────────
|
||||
app.delete('/api/files', (req, res) => {
|
||||
app.delete('/api/files', authMiddleware, (req, res) => {
|
||||
const { ids } = req.body;
|
||||
if (!Array.isArray(ids) || !ids.length) return res.status(400).json({ error: 'ids required' });
|
||||
|
||||
@@ -344,8 +352,28 @@ app.delete('/api/files', (req, res) => {
|
||||
res.json({ deleted: ids.length });
|
||||
});
|
||||
|
||||
// ── API: 관리자 인증 ──────────────────────────────────────────
|
||||
app.post('/api/admin/login', (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (password !== ADMIN_PASSWORD) return res.status(401).json({ error: '비밀번호가 올바르지 않습니다' });
|
||||
const token = randomUUID();
|
||||
sessions.add(token);
|
||||
res.json({ token });
|
||||
});
|
||||
|
||||
app.post('/api/admin/logout', (req, res) => {
|
||||
const token = (req.headers.authorization || '').replace('Bearer ', '').trim();
|
||||
sessions.delete(token);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/admin/verify', (req, res) => {
|
||||
const token = (req.headers.authorization || '').replace('Bearer ', '').trim();
|
||||
res.json({ ok: sessions.has(token) });
|
||||
});
|
||||
|
||||
// ── API: 관리자 파일 목록 (검색·페이지네이션) ─────────────────
|
||||
app.get('/api/admin/files', (req, res) => {
|
||||
app.get('/api/admin/files', authMiddleware, (req, res) => {
|
||||
const { search, category, page = 1, limit = 20 } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(limit);
|
||||
const where = [];
|
||||
@@ -370,7 +398,7 @@ app.get('/api/admin/files', (req, res) => {
|
||||
});
|
||||
|
||||
// ── API: 파일 메타데이터 수정 ─────────────────────────────────
|
||||
app.patch('/api/files/:id', (req, res) => {
|
||||
app.patch('/api/files/:id', authMiddleware, (req, res) => {
|
||||
const row = queryGet('SELECT * FROM files WHERE id = :id', { ':id': req.params.id });
|
||||
if (!row) return res.status(404).json({ error: 'not found' });
|
||||
const { name, category, author, visibility, modified_at } = req.body;
|
||||
@@ -396,7 +424,7 @@ app.get('/api/categories', (req, res) => {
|
||||
res.json(queryAll('SELECT * FROM categories ORDER BY sort_order ASC, id ASC'));
|
||||
});
|
||||
|
||||
app.post('/api/categories', (req, res) => {
|
||||
app.post('/api/categories', authMiddleware, (req, res) => {
|
||||
const { id, label, icon = 'folder', sort_order = 0 } = req.body;
|
||||
if (!id || !label) return res.status(400).json({ error: 'id와 label은 필수입니다' });
|
||||
const exists = queryGet('SELECT id FROM categories WHERE id = :id', { ':id': id });
|
||||
@@ -409,7 +437,7 @@ app.post('/api/categories', (req, res) => {
|
||||
res.status(201).json(queryGet('SELECT * FROM categories WHERE id = :id', { ':id': id }));
|
||||
});
|
||||
|
||||
app.patch('/api/categories/:id', (req, res) => {
|
||||
app.patch('/api/categories/:id', authMiddleware, (req, res) => {
|
||||
const row = queryGet('SELECT * FROM categories WHERE id = :id', { ':id': req.params.id });
|
||||
if (!row) return res.status(404).json({ error: 'not found' });
|
||||
const { label, icon, sort_order } = req.body;
|
||||
@@ -424,7 +452,7 @@ app.patch('/api/categories/:id', (req, res) => {
|
||||
res.json(queryGet('SELECT * FROM categories WHERE id = :id', { ':id': req.params.id }));
|
||||
});
|
||||
|
||||
app.delete('/api/categories/:id', (req, res) => {
|
||||
app.delete('/api/categories/:id', authMiddleware, (req, res) => {
|
||||
const row = queryGet('SELECT * FROM categories WHERE id = :id', { ':id': req.params.id });
|
||||
if (!row) return res.status(404).json({ error: 'not found' });
|
||||
const used = queryGet('SELECT COUNT(*) as c FROM files WHERE category = :id', { ':id': req.params.id });
|
||||
@@ -443,7 +471,7 @@ app.get('/api/users', (req, res) => {
|
||||
res.json(withCount);
|
||||
});
|
||||
|
||||
app.post('/api/users', (req, res) => {
|
||||
app.post('/api/users', authMiddleware, (req, res) => {
|
||||
const { name, email = null, role = 'member' } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'name은 필수입니다' });
|
||||
const exists = queryGet('SELECT id FROM users WHERE name = :name', { ':name': name });
|
||||
@@ -457,7 +485,7 @@ app.post('/api/users', (req, res) => {
|
||||
res.status(201).json(queryGet('SELECT * FROM users WHERE id = :id', { ':id': id }));
|
||||
});
|
||||
|
||||
app.patch('/api/users/:id', (req, res) => {
|
||||
app.patch('/api/users/:id', authMiddleware, (req, res) => {
|
||||
const row = queryGet('SELECT * FROM users WHERE id = :id', { ':id': req.params.id });
|
||||
if (!row) return res.status(404).json({ error: 'not found' });
|
||||
const { name, email, role } = req.body;
|
||||
@@ -477,7 +505,7 @@ app.patch('/api/users/:id', (req, res) => {
|
||||
res.json(queryGet('SELECT * FROM users WHERE id = :id', { ':id': req.params.id }));
|
||||
});
|
||||
|
||||
app.delete('/api/users/:id', (req, res) => {
|
||||
app.delete('/api/users/:id', authMiddleware, (req, res) => {
|
||||
const row = queryGet('SELECT * FROM users WHERE id = :id', { ':id': req.params.id });
|
||||
if (!row) return res.status(404).json({ error: 'not found' });
|
||||
const fc = queryGet('SELECT COUNT(*) as c FROM files WHERE author = :name', { ':name': row.name });
|
||||
@@ -487,5 +515,10 @@ app.delete('/api/users/:id', (req, res) => {
|
||||
|
||||
// ── 시작 ──────────────────────────────────────────────────────
|
||||
initDb().then(() => {
|
||||
app.listen(PORT, () => console.log(`자료실 서버 실행 중 → http://localhost:${PORT}`));
|
||||
app.listen(PORT, () => {
|
||||
console.log(`자료실 서버 실행 중 → http://localhost:${PORT}`);
|
||||
if (!process.env.ADMIN_PASSWORD) {
|
||||
console.warn('⚠️ 관리자 비밀번호가 기본값(admin1234)입니다. ADMIN_PASSWORD 환경변수로 변경하세요.');
|
||||
}
|
||||
});
|
||||
}).catch(err => { console.error('DB 초기화 실패:', err); process.exit(1); });
|
||||
|
||||
Reference in New Issue
Block a user