- Added Express.js admin panel on port 3001 (ADMIN_PORT env) - Dashboard: stats (users, products, purchases, revenue) - Users: list, details, ban/unban toggle - Products: CRUD by category - Wallets: list with balances - Purchases: history with filters - Audit log: view audit trail - Auth: token-based login with ADMIN_SECRET env var - Migrated sqlite3 → better-sqlite3 - database.js: async adapter (runAsync/allAsync/getAsync) - purchaseService.js: lastID → lastInsertRowid - userService.js: lastID → lastInsertRowid - Removed sqlite3 from package.json - Fixed: dotenv/config import added to index.js - Fixed: ENCRYPTION_KEY validation (32+ char hex) - Fixed: Dockerfile multi-stage build (no python needed) - Fixed: Docker DNS (network: host in build) - Fixed: docker-compose port 3001, healthcheck on 3001 - Added express, cookie-parser, pino-pretty, better-sqlite3 deps
84 lines
2.3 KiB
JavaScript
84 lines
2.3 KiB
JavaScript
import crypto from 'crypto';
|
|
import config from '../config/config.js';
|
|
|
|
const TOKEN_SECRET = process.env.ADMIN_SECRET || config.ADMIN_IDS[0] || 'change-me';
|
|
const COOKIE_NAME = 'admin_token';
|
|
const MAX_AGE = 24 * 60 * 60 * 1000;
|
|
|
|
function signToken(data) {
|
|
const payload = JSON.stringify({ ...data, exp: Date.now() + MAX_AGE });
|
|
const b64 = Buffer.from(payload).toString('base64');
|
|
const sig = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex');
|
|
return `${b64}.${sig}`;
|
|
}
|
|
|
|
function verifyToken(token) {
|
|
try {
|
|
const [b64, sig] = token.split('.');
|
|
const expected = crypto.createHmac('sha256', TOKEN_SECRET).update(b64).digest('hex');
|
|
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null;
|
|
const payload = JSON.parse(Buffer.from(b64, 'base64').toString());
|
|
if (payload.exp < Date.now()) return null;
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function requireAuth(req, res, next) {
|
|
const token = req.cookies?.[COOKIE_NAME];
|
|
if (!token) return res.redirect('/login');
|
|
const payload = verifyToken(token);
|
|
if (!payload) {
|
|
res.clearCookie(COOKIE_NAME);
|
|
return res.redirect('/login');
|
|
}
|
|
req.admin = payload;
|
|
next();
|
|
}
|
|
|
|
export function handleLogin(req, res) {
|
|
const { token } = req.body || {};
|
|
if (token !== TOKEN_SECRET) {
|
|
return res.status(401).send(renderLogin('Invalid token'));
|
|
}
|
|
const signed = signToken({ role: 'admin' });
|
|
res.cookie(COOKIE_NAME, signed, {
|
|
httpOnly: true,
|
|
sameSite: 'strict',
|
|
maxAge: MAX_AGE,
|
|
secure: false
|
|
});
|
|
res.redirect('/');
|
|
}
|
|
|
|
export function handleLogout(req, res) {
|
|
res.clearCookie(COOKIE_NAME);
|
|
res.redirect('/login');
|
|
}
|
|
|
|
function renderLogin(error) {
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Login</title>
|
|
<link rel="stylesheet" href="/admin/style.css">
|
|
</head>
|
|
<body class="login-page">
|
|
<div class="login-box">
|
|
<h1>Admin Panel</h1>
|
|
${error ? `<p class="error">${error}</p>` : ''}
|
|
<form method="POST" action="/login">
|
|
<label for="token">Admin Token</label>
|
|
<input type="password" id="token" name="token" required autofocus>
|
|
<button type="submit">Login</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
export { renderLogin };
|