Files
telegram-shop/src/admin/auth.js
NW 4657b1dfb5 feat: web admin panel + better-sqlite3 migration + Docker fixes
- 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
2026-06-22 10:54:01 +01:00

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 };