require('dotenv').config({ path: require('path').join(__dirname, '.env') }); const express = require('express'); const db = require('./db'); const auth = require('./auth'); const rateLimit = require('./rate_limit'); const path = require('path'); const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Init DB db.initDB(); // GET /api/status app.get('/api/status', (req, res) => { res.json({ status: 'ok', server_time: new Date().toISOString() }); }); // POST /api/login app.post('/api/login', async (req, res) => { const { username, password_hash, hwid_components } = req.body; const ip = req.ip || req.socket.remoteAddress || 'unknown'; const userAgent = req.headers['user-agent'] || ''; if (!username || !password_hash || !hwid_components) { return res.status(400).json({ success: false, error: 'MISSING_FIELDS' }); } const { cpu, board, disk, mac } = hwid_components; if (!cpu || !board || !disk || !mac) { return res.status(400).json({ success: false, error: 'MISSING_HWID_COMPONENTS' }); } const hwidCombined = auth.combinedHwidHash(hwid_components); // Rate limit const rl = rateLimit.checkLoginRateLimit(ip, username, hwidCombined); if (!rl.allowed) { return res.status(429).json({ success: false, error: 'RATE_LIMITED', message: 'Too many attempts', retry_after: rl.retryAfter }); } // User lookup const user = db.getUserByUsername(username); if (!user) { db.logLogin(null, null, ip, userAgent, 0, false, 'USER_NOT_FOUND'); return res.json({ success: false, error: 'INVALID_CREDENTIALS' }); } if (!user.is_active) return res.json({ success: false, error: 'ACCOUNT_DISABLED' }); if (user.expires_at && new Date(user.expires_at) < new Date()) return res.json({ success: false, error: 'ACCOUNT_EXPIRED' }); // Verify password (Argon2id) let passwordValid = false; try { passwordValid = await auth.verifyPassword(user.password_hash, password_hash); } catch (err) { console.error('[login] argon2 error:', err.message); return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' }); } if (!passwordValid) { db.logLogin(user.id, null, ip, userAgent, 0, false, 'WRONG_PASSWORD'); return res.json({ success: false, error: 'INVALID_CREDENTIALS' }); } // Admin accounts skip HWID check (for web panel access) if (user.role === 'admin') { const accessToken = auth.signAccessToken(user, 0); const refreshToken = auth.generateRefreshToken(); const tokenHash = auth.hashToken(refreshToken); const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(); db.saveRefreshToken(tokenHash, user.id, 0, 'admin', expiresAt); db.logLogin(user.id, 0, ip, userAgent, 1.0, true, null); return res.json({ success: true, access_token: accessToken, refresh_token: refreshToken, username: user.username, match_rate: 1.0 }); } // HWID matching const userDevices = db.findMatchingDevice(user.id, cpu, board, disk, mac); let matchedDevice = null; let bestRate = 0; for (const device of userDevices) { const result = auth.hwidMatchRate(device, hwid_components); if (result.match && result.rate > bestRate) { matchedDevice = device; bestRate = result.rate; } } if (!matchedDevice) { if (userDevices.length === 0) { // First login — auto-create device const displayName = req.headers['x-device-name'] || 'Unknown Device'; const result = db.createDevice(user.id, displayName, cpu, board, disk, mac, ip); matchedDevice = { id: result.lastInsertRowid, cpu_hash: cpu, board_hash: board, disk_hash: disk, mac_hash: mac }; bestRate = 1.0; } else { // HWID threshold failed db.logLogin(user.id, null, ip, userAgent, bestRate, false, 'HWID_THRESHOLD'); return res.json({ success: false, error: 'HWID_THRESHOLD', message: 'Hardware does not match any bound device', match_rate: bestRate }); } } else { db.updateDeviceLogin(matchedDevice.id, ip); } // Generate tokens const accessToken = auth.signAccessToken(user, matchedDevice.id); const refreshToken = auth.generateRefreshToken(); const tokenHash = auth.hashToken(refreshToken); const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(); db.saveRefreshToken(tokenHash, user.id, matchedDevice.id, hwidCombined, expiresAt); db.logLogin(user.id, matchedDevice.id, ip, userAgent, bestRate, true, null); res.json({ success: true, access_token: accessToken, refresh_token: refreshToken, username: user.username, match_rate: bestRate }); }); // POST /api/refresh app.post('/api/refresh', (req, res) => { const { refresh_token, hwid_components } = req.body; if (!refresh_token || !hwid_components) { return res.status(400).json({ success: false, error: 'MISSING_FIELDS' }); } const tokenHash = auth.hashToken(refresh_token); const stored = db.findRefreshToken(tokenHash); if (!stored || stored.revoked) return res.json({ success: false, error: 'TOKEN_EXPIRED' }); if (new Date(stored.expires_at) < new Date()) return res.json({ success: false, error: 'TOKEN_EXPIRED' }); // Verify HWID const hwidCombined = auth.combinedHwidHash(hwid_components); if (stored.hwid_hash !== hwidCombined) { const user = db.getUserById(stored.user_id); if (!user) return res.json({ success: false, error: 'TOKEN_EXPIRED' }); const userDevices = db.findMatchingDevice(user.id, hwid_components.cpu, hwid_components.board, hwid_components.disk, hwid_components.mac); let matched = false; for (const device of userDevices) { if (auth.hwidMatchRate(device, hwid_components).match) { matched = true; break; } } if (!matched) return res.json({ success: false, error: 'HWID_THRESHOLD' }); } // Rotate db.revokeRefreshToken(tokenHash); const user = db.getUserById(stored.user_id); const newAccessToken = auth.signAccessToken(user, stored.device_id); const newRefreshToken = auth.generateRefreshToken(); const newTokenHash = auth.hashToken(newRefreshToken); const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(); db.saveRefreshToken(newTokenHash, user.id, stored.device_id, auth.combinedHwidHash(hwid_components), expiresAt); if (stored.device_id) db.updateDeviceLogin(stored.device_id, req.ip || req.socket.remoteAddress); res.json({ success: true, access_token: newAccessToken, refresh_token: newRefreshToken }); }); // POST /api/logout app.post('/api/logout', (req, res) => { const { refresh_token } = req.body; if (refresh_token) db.revokeRefreshToken(auth.hashToken(refresh_token)); res.json({ success: true }); }); // POST /api/register (public self-registration) app.post('/api/register', async (req, res) => { const { username, password_hash } = req.body; const ip = req.ip || req.socket.remoteAddress || 'unknown'; if (!username || !password_hash) { return res.status(400).json({ success: false, error: 'MISSING_FIELDS' }); } // Username rules if (username.length < 3 || username.length > 32) { return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Username must be 3-32 characters' }); } if (!/^[a-zA-Z0-9_]+$/.test(username)) { return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Only letters, numbers, underscores' }); } // Rate limit registration per IP (5 per hour) const rl = db.checkRateLimit(`register:${ip}`, 'register', 5, 3600); if (!rl.allowed) { return res.status(429).json({ success: false, error: 'RATE_LIMITED', message: 'Too many registrations. Try again later.', retry_after: rl.retryAfter }); } // Check if username taken if (db.getUserByUsername(username)) { return res.json({ success: false, error: 'USERNAME_TAKEN' }); } // password_hash comes as SHA-256 from client — verify it's 64-char hex if (password_hash.length !== 64 || !/^[a-f0-9]{64}$/.test(password_hash)) { return res.json({ success: false, error: 'INVALID_PASSWORD_HASH' }); } // password_hash is already SHA-256(password) — Argon2id it directly let storedHash; try { storedHash = await auth.hashPasswordFromHash(password_hash); } catch (err) { return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' }); } db.createUser(username, storedHash, 'user', null); console.log(`[register] New user: ${username} (ip: ${ip})`); res.json({ success: true, message: 'Registration successful. You can now login.' }); }); // Admin: POST /api/admin/create-user app.post('/api/admin/create-user', auth.requireAdmin, async (req, res) => { const { username, password, role, expires_at } = req.body; if (!username || !password) { return res.status(400).json({ success: false, error: 'MISSING_FIELDS' }); } if (db.getUserByUsername(username)) { return res.json({ success: false, error: 'USERNAME_TAKEN' }); } let passwordHash; try { passwordHash = await auth.hashPassword(password); } catch (err) { return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' }); } db.createUser(username, passwordHash, role || 'user', expires_at || null); res.json({ success: true, message: `User '${username}' created` }); }); // Admin: GET /api/admin/users (list all users) app.get('/api/admin/users', auth.requireAdmin, (req, res) => { const db = require('./db'); const users = db.getDB().prepare( `SELECT u.id, u.username, u.role, u.is_active, u.created_at, u.expires_at, (SELECT MIN(d.created_at) FROM devices d WHERE d.user_id = u.id) as hwid_bound_at FROM users u ORDER BY u.id DESC` ).all(); res.json({ users }); }); // Admin: GET /api/admin/logs (recent login logs, last 100) app.get('/api/admin/logs', auth.requireAdmin, (req, res) => { const db = require('./db'); const logs = db.getDB().prepare( `SELECT login_log.*, users.username FROM login_log LEFT JOIN users ON login_log.user_id = users.id ORDER BY login_log.id DESC LIMIT 100` ).all(); res.json({ logs }); }); // Admin: GET /api/admin/stats app.get('/api/admin/stats', auth.requireAdmin, (req, res) => { const db = require('./db'); const totalUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users').get().cnt; const activeUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users WHERE is_active = 1').get().cnt; const logins24h = db.getDB().prepare( "SELECT COUNT(*) as cnt FROM login_log WHERE success = 1 AND attempted_at > datetime('now', '-1 day')" ).get().cnt; const failures24h = db.getDB().prepare( "SELECT COUNT(*) as cnt FROM login_log WHERE success = 0 AND attempted_at > datetime('now', '-1 day')" ).get().cnt; res.json({ totalUsers, activeUsers, logins24h, failures24h }); }); // Admin: POST /api/admin/user/:id/toggle (ban/unban) app.post('/api/admin/user/:id/toggle', auth.requireAdmin, (req, res) => { const db = require('./db'); const user = db.getUserById(req.params.id); if (!user) return res.json({ success: false, error: 'NOT_FOUND' }); const newStatus = user.is_active ? 0 : 1; db.getDB().prepare('UPDATE users SET is_active = ? WHERE id = ?').run(newStatus, req.params.id); res.json({ success: true, is_active: newStatus }); }); // Admin: POST /api/admin/user/:id/reset-hwid app.post('/api/admin/user/:id/reset-hwid', auth.requireAdmin, (req, res) => { const db = require('./db'); db.getDB().prepare('DELETE FROM devices WHERE user_id = ?').run(req.params.id); db.getDB().prepare( 'UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?' ).run(req.params.id); res.json({ success: true, message: 'HWID reset. User can bind to new machine on next login.' }); }); // Start const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`[server] running on port ${PORT}`); console.log(` GET /api/status`); console.log(` POST /api/login`); console.log(` POST /api/refresh`); console.log(` POST /api/logout`); console.log(` POST /api/admin/create-user (admin)`); });