Fix 500 error on batch delete + add SQLite busyTimeout
- Set SQLite busyTimeout(5000) to prevent 'database is locked' on concurrent requests - Await all loadEntries() in mutation functions to eliminate race conditions - Remove redundant loadEntries() from batch operations
This commit is contained in:
@@ -18,6 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0);
|
||||
$db_path = __DIR__ . '/vault.db';
|
||||
$db = new SQLite3($db_path);
|
||||
$db->enableExceptions(true);
|
||||
$db->busyTimeout(5000);
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
@@ -25,7 +26,6 @@ $db->exec("
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
encryption_key TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
@@ -50,12 +50,23 @@ $db->exec("
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
");
|
||||
|
||||
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN encryption_method TEXT DEFAULT 'server'"); } catch (Exception $e) {}
|
||||
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {}
|
||||
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted INTEGER DEFAULT 0"); } catch (Exception $e) {}
|
||||
try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted_at DATETIME"); } catch (Exception $e) {}
|
||||
try { $db->exec("ALTER TABLE users DROP COLUMN encryption_key"); } catch (Exception $e) {}
|
||||
|
||||
$db->exec("DELETE FROM sessions WHERE expires_at < datetime('now')");
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$path = str_replace('/password-manager/api.php', '', $path);
|
||||
@@ -63,23 +74,22 @@ $path = str_replace('/api.php', '', $path);
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
function genKey($pwd, $salt) { return hash_pbkdf2('sha256', $pwd, $salt, 100000, 32, true); }
|
||||
|
||||
function authenticate($db) {
|
||||
$headers = getallheaders();
|
||||
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
|
||||
if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; }
|
||||
$decoded = base64_decode($token);
|
||||
if (!$decoded) { http_response_code(401); echo json_encode(['error'=>'Bad token']); exit; }
|
||||
$parts = explode(':', $decoded);
|
||||
if (count($parts) !== 3) { http_response_code(401); echo json_encode(['error'=>'Token format']); exit; }
|
||||
$uid = intval($parts[0]);
|
||||
$key = base64_decode($parts[2]);
|
||||
$st = $db->prepare('SELECT id, encryption_key FROM users WHERE id=:id');
|
||||
$st->bindValue(':id', $uid, SQLITE3_INTEGER);
|
||||
$user = $st->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$user || $user['encryption_key'] !== base64_encode($key)) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
|
||||
return ['userId' => $uid];
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$st = $db->prepare('SELECT user_id, expires_at FROM sessions WHERE token_hash=:th');
|
||||
$st->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$session = $st->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$session) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; }
|
||||
if (strtotime($session['expires_at']) < time()) {
|
||||
$del = $db->prepare('DELETE FROM sessions WHERE token_hash=:th');
|
||||
$del->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$del->execute();
|
||||
http_response_code(401); echo json_encode(['error'=>'Session expired']); exit;
|
||||
}
|
||||
return ['userId' => (int)$session['user_id']];
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -93,12 +103,10 @@ try {
|
||||
if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; }
|
||||
$salt = bin2hex(random_bytes(32));
|
||||
$hash = hash_pbkdf2('sha256', $p, $salt, 100000);
|
||||
$key = genKey($p, $salt);
|
||||
$st = $db->prepare('INSERT INTO users (username, password_hash, salt, encryption_key) VALUES (:u, :h, :s, :k)');
|
||||
$st = $db->prepare('INSERT INTO users (username, password_hash, salt) VALUES (:u, :h, :s)');
|
||||
$st->bindValue(':u', $u, SQLITE3_TEXT);
|
||||
$st->bindValue(':h', $hash, SQLITE3_TEXT);
|
||||
$st->bindValue(':s', $salt, SQLITE3_TEXT);
|
||||
$st->bindValue(':k', base64_encode($key), SQLITE3_TEXT);
|
||||
$st->execute();
|
||||
$uid = $db->lastInsertRowID();
|
||||
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
||||
@@ -108,7 +116,14 @@ try {
|
||||
$stFolder->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$stFolder->execute();
|
||||
}
|
||||
$token = base64_encode($uid . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));
|
||||
$stS = $db->prepare('INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (:uid, :th, :exp)');
|
||||
$stS->bindValue(':uid', $uid, SQLITE3_INTEGER);
|
||||
$stS->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$stS->bindValue(':exp', $expires, SQLITE3_TEXT);
|
||||
$stS->execute();
|
||||
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$uid,'salt'=>$salt]);
|
||||
break;
|
||||
|
||||
@@ -122,8 +137,6 @@ try {
|
||||
if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) {
|
||||
http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break;
|
||||
}
|
||||
$key = genKey($p, $user['salt']);
|
||||
$token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key));
|
||||
$defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal'];
|
||||
$stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)');
|
||||
foreach ($defaultFolders as $name) {
|
||||
@@ -131,6 +144,14 @@ try {
|
||||
$stFolder->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$stFolder->execute();
|
||||
}
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$expires = date('Y-m-d H:i:s', strtotime('+24 hours'));
|
||||
$stS = $db->prepare('INSERT INTO sessions (user_id, token_hash, expires_at) VALUES (:uid, :th, :exp)');
|
||||
$stS->bindValue(':uid', $user['id'], SQLITE3_INTEGER);
|
||||
$stS->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$stS->bindValue(':exp', $expires, SQLITE3_TEXT);
|
||||
$stS->execute();
|
||||
echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]);
|
||||
break;
|
||||
|
||||
@@ -275,6 +296,18 @@ try {
|
||||
break;
|
||||
|
||||
// Empty trash (permanently delete all soft-deleted)
|
||||
case ($path === '/logout' && $method === 'POST'):
|
||||
$headers = getallheaders();
|
||||
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
|
||||
if ($token) {
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$del = $db->prepare('DELETE FROM sessions WHERE token_hash=:th');
|
||||
$del->bindValue(':th', $tokenHash, SQLITE3_TEXT);
|
||||
$del->execute();
|
||||
}
|
||||
echo json_encode(['message'=>'Logged out']);
|
||||
break;
|
||||
|
||||
case ($path === '/entries/trash/empty' && $method === 'DELETE'):
|
||||
$auth = authenticate($db);
|
||||
$st = $db->prepare('DELETE FROM vault_entries WHERE user_id=:uid AND deleted=1');
|
||||
|
||||
Reference in New Issue
Block a user