main
This commit is contained in:
+198
@@ -0,0 +1,198 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
let db;
|
||||
|
||||
function initDB(dbPath) {
|
||||
db = new Database(dbPath || path.join(__dirname, 'skeet_server.db'));
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
cpu_hash TEXT NOT NULL,
|
||||
board_hash TEXT NOT NULL,
|
||||
disk_hash TEXT NOT NULL,
|
||||
mac_hash TEXT NOT NULL,
|
||||
last_login TEXT,
|
||||
last_ip TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token_hash TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id INTEGER,
|
||||
hwid_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS login_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
device_id INTEGER,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
hwid_match_rate REAL,
|
||||
success INTEGER NOT NULL,
|
||||
reason TEXT,
|
||||
attempted_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
key TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
count INTEGER NOT NULL DEFAULT 1,
|
||||
window_start TEXT NOT NULL,
|
||||
PRIMARY KEY (key, action)
|
||||
);
|
||||
`);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function getDB() {
|
||||
if (!db) throw new Error('Database not initialized. Call initDB() first.');
|
||||
return db;
|
||||
}
|
||||
|
||||
// ─── Users ────────────────────────────────────────────
|
||||
|
||||
function getUserByUsername(username) {
|
||||
return getDB().prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
}
|
||||
|
||||
function getUserById(id) {
|
||||
return getDB().prepare('SELECT * FROM users WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
function createUser(username, passwordHash, role = 'user', expiresAt = null) {
|
||||
return getDB().prepare(
|
||||
'INSERT INTO users (username, password_hash, role, expires_at) VALUES (?, ?, ?, ?)'
|
||||
).run(username, passwordHash, role, expiresAt);
|
||||
}
|
||||
|
||||
// ─── Devices ──────────────────────────────────────────
|
||||
|
||||
function findMatchingDevice(userId, cpuHash, boardHash, diskHash, macHash) {
|
||||
const devices = getDB().prepare(
|
||||
'SELECT * FROM devices WHERE user_id = ?'
|
||||
).all(userId);
|
||||
return devices;
|
||||
}
|
||||
|
||||
function createDevice(userId, displayName, cpuHash, boardHash, diskHash, macHash, ip) {
|
||||
return getDB().prepare(
|
||||
`INSERT INTO devices (user_id, display_name, cpu_hash, board_hash, disk_hash, mac_hash, last_login, last_ip)
|
||||
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), ?)`
|
||||
).run(userId, displayName, cpuHash, boardHash, diskHash, macHash, ip);
|
||||
}
|
||||
|
||||
function updateDeviceLogin(deviceId, ip) {
|
||||
getDB().prepare(
|
||||
'UPDATE devices SET last_login = datetime(\'now\'), last_ip = ? WHERE id = ?'
|
||||
).run(ip, deviceId);
|
||||
}
|
||||
|
||||
// ─── Refresh Tokens ───────────────────────────────────
|
||||
|
||||
function saveRefreshToken(tokenHash, userId, deviceId, hwidHash, expiresAt) {
|
||||
return getDB().prepare(
|
||||
'INSERT INTO refresh_tokens (token_hash, user_id, device_id, hwid_hash, expires_at) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tokenHash, userId, deviceId, hwidHash, expiresAt);
|
||||
}
|
||||
|
||||
function findRefreshToken(tokenHash) {
|
||||
return getDB().prepare(
|
||||
'SELECT * FROM refresh_tokens WHERE token_hash = ?'
|
||||
).get(tokenHash);
|
||||
}
|
||||
|
||||
function revokeRefreshToken(tokenHash) {
|
||||
getDB().prepare(
|
||||
'UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?'
|
||||
).run(tokenHash);
|
||||
}
|
||||
|
||||
// ─── Login Log ────────────────────────────────────────
|
||||
|
||||
function logLogin(userId, deviceId, ip, userAgent, hwidMatchRate, success, reason) {
|
||||
getDB().prepare(
|
||||
`INSERT INTO login_log (user_id, device_id, ip_address, user_agent, hwid_match_rate, success, reason)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(userId, deviceId, ip, userAgent, hwidMatchRate, success ? 1 : 0, reason);
|
||||
}
|
||||
|
||||
function countRecentFailures(username, windowMinutes = 10) {
|
||||
const row = getDB().prepare(
|
||||
`SELECT COUNT(*) as cnt FROM login_log
|
||||
JOIN users ON login_log.user_id = users.id
|
||||
WHERE users.username = ? AND login_log.success = 0
|
||||
AND login_log.attempted_at > datetime('now', '-' || ? || ' minutes')`
|
||||
).get(username, windowMinutes);
|
||||
return row ? row.cnt : 0;
|
||||
}
|
||||
|
||||
// ─── Rate Limits ──────────────────────────────────────
|
||||
|
||||
function checkRateLimit(key, action, maxCount, windowSeconds) {
|
||||
const now = new Date().toISOString();
|
||||
const db = getDB();
|
||||
|
||||
// UPSERT — single row per (key, action)
|
||||
db.prepare(
|
||||
`INSERT INTO rate_limits (key, action, count, window_start)
|
||||
VALUES (?, ?, 1, ?)
|
||||
ON CONFLICT(key, action) DO UPDATE SET
|
||||
count = CASE
|
||||
WHEN (unixepoch(?) - unixepoch(window_start)) > ? THEN 1
|
||||
ELSE count + 1
|
||||
END,
|
||||
window_start = CASE
|
||||
WHEN (unixepoch(?) - unixepoch(window_start)) > ? THEN ?
|
||||
ELSE window_start
|
||||
END`
|
||||
).run(key, action, now, now, windowSeconds, now, windowSeconds, now);
|
||||
|
||||
const row = db.prepare(
|
||||
'SELECT count, window_start FROM rate_limits WHERE key = ? AND action = ?'
|
||||
).get(key, action);
|
||||
|
||||
if (!row) return { allowed: true, remaining: maxCount };
|
||||
|
||||
const windowAge = (Date.now() - new Date(row.window_start + 'Z').getTime()) / 1000;
|
||||
if (windowAge > windowSeconds) return { allowed: true, remaining: maxCount };
|
||||
|
||||
return {
|
||||
allowed: row.count <= maxCount,
|
||||
remaining: Math.max(0, maxCount - row.count),
|
||||
retryAfter: row.count > maxCount ? Math.ceil(windowSeconds - windowAge) : 0
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initDB, getDB,
|
||||
getUserByUsername, getUserById, createUser,
|
||||
findMatchingDevice, createDevice, updateDeviceLogin,
|
||||
saveRefreshToken, findRefreshToken, revokeRefreshToken,
|
||||
logLogin, countRecentFailures,
|
||||
checkRateLimit
|
||||
};
|
||||
Reference in New Issue
Block a user