Add comprehensive database setup and user management system
- Implement PostgreSQL database schema with users and bookmarks tables - Add database connection pooling with retry logic and error handling - Create migration system with automatic schema initialization - Add database CLI tools for management (init, status, validate, etc.) - Include comprehensive error handling and diagnostics - Add development seed data and testing utilities - Implement health monitoring and connection pool statistics - Create detailed documentation and troubleshooting guide Database features: - Users table with authentication fields and email verification - Bookmarks table with user association and metadata - Proper indexes for performance optimization - Automatic timestamp triggers - Transaction support with rollback handling - Connection pooling (20 max connections, 30s idle timeout) - Graceful shutdown handling CLI commands available: - npm run db:init - Initialize database - npm run db:status - Check database status - npm run db:validate - Validate schema - npm run db:test - Run database tests - npm run db:diagnostics - Full diagnostics
This commit is contained in:
392
tests/test_security_features.html
Normal file
392
tests/test_security_features.html
Normal file
@ -0,0 +1,392 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Security Features Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.test-result {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.test-pass {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.test-fail {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
button {
|
||||
margin: 5px;
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Security Features Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Encryption Tests</h2>
|
||||
<button onclick="testEncryption()">Test Encryption</button>
|
||||
<div id="encryptionResults"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Privacy Mode Tests</h2>
|
||||
<button onclick="testPrivacyMode()">Test Privacy Mode</button>
|
||||
<div id="privacyResults"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Access Logging Tests</h2>
|
||||
<button onclick="testAccessLogging()">Test Access Logging</button>
|
||||
<div id="loggingResults"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Password Protection Tests</h2>
|
||||
<button onclick="testPasswordProtection()">Test Password Protection</button>
|
||||
<div id="passwordResults"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Mock BookmarkManager for testing
|
||||
class MockBookmarkManager {
|
||||
constructor() {
|
||||
this.securitySettings = {
|
||||
encryptionEnabled: false,
|
||||
encryptionKey: null,
|
||||
privacyMode: false,
|
||||
accessLogging: true,
|
||||
passwordProtection: false,
|
||||
sessionTimeout: 30 * 60 * 1000,
|
||||
maxLoginAttempts: 3,
|
||||
lockoutDuration: 15 * 60 * 1000
|
||||
};
|
||||
|
||||
this.accessLog = [];
|
||||
this.encryptedCollections = new Set();
|
||||
this.privateBookmarks = new Set();
|
||||
this.securitySession = {
|
||||
isAuthenticated: false,
|
||||
lastActivity: Date.now(),
|
||||
loginAttempts: 0,
|
||||
lockedUntil: null
|
||||
};
|
||||
|
||||
this.bookmarks = [
|
||||
{ id: '1', title: 'Test Bookmark 1', url: 'https://example.com' },
|
||||
{ id: '2', title: 'Test Bookmark 2', url: 'https://test.com' }
|
||||
];
|
||||
}
|
||||
|
||||
// Hash password for storage (simple implementation)
|
||||
hashPassword(password) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
const char = password.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
// Simple encryption function
|
||||
simpleEncrypt(text, key) {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(text.charCodeAt(i) ^ key.toString().charCodeAt(i % key.toString().length));
|
||||
}
|
||||
return btoa(result);
|
||||
}
|
||||
|
||||
// Simple decryption function
|
||||
simpleDecrypt(encryptedText, key) {
|
||||
try {
|
||||
const decoded = atob(encryptedText);
|
||||
let result = '';
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
result += String.fromCharCode(decoded.charCodeAt(i) ^ key.toString().charCodeAt(i % key.toString().length));
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
return encryptedText;
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt bookmark data
|
||||
encryptBookmark(bookmark) {
|
||||
if (!this.securitySettings.encryptionEnabled || !this.securitySettings.encryptionKey) {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = this.securitySettings.encryptionKey;
|
||||
const encryptedTitle = this.simpleEncrypt(bookmark.title, key);
|
||||
const encryptedUrl = this.simpleEncrypt(bookmark.url, key);
|
||||
|
||||
return {
|
||||
...bookmark,
|
||||
title: encryptedTitle,
|
||||
url: encryptedUrl,
|
||||
encrypted: true
|
||||
};
|
||||
} catch (error) {
|
||||
return bookmark;
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt bookmark data
|
||||
decryptBookmark(bookmark) {
|
||||
if (!bookmark.encrypted || !this.securitySettings.encryptionKey) {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = this.securitySettings.encryptionKey;
|
||||
const decryptedTitle = this.simpleDecrypt(bookmark.title, key);
|
||||
const decryptedUrl = this.simpleDecrypt(bookmark.url, key);
|
||||
|
||||
return {
|
||||
...bookmark,
|
||||
title: decryptedTitle,
|
||||
url: decryptedUrl
|
||||
};
|
||||
} catch (error) {
|
||||
return bookmark;
|
||||
}
|
||||
}
|
||||
|
||||
// Log access events
|
||||
logAccess(action, details = {}) {
|
||||
if (!this.securitySettings.accessLogging) return;
|
||||
|
||||
const logEntry = {
|
||||
timestamp: Date.now(),
|
||||
action: action,
|
||||
details: details,
|
||||
sessionId: 'test_session'
|
||||
};
|
||||
|
||||
this.accessLog.push(logEntry);
|
||||
}
|
||||
|
||||
// Toggle bookmark privacy
|
||||
toggleBookmarkPrivacy(bookmarkId) {
|
||||
if (this.privateBookmarks.has(bookmarkId)) {
|
||||
this.privateBookmarks.delete(bookmarkId);
|
||||
} else {
|
||||
this.privateBookmarks.add(bookmarkId);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if bookmark is private
|
||||
isBookmarkPrivate(bookmarkId) {
|
||||
return this.privateBookmarks.has(bookmarkId);
|
||||
}
|
||||
|
||||
// Filter bookmarks for export
|
||||
getExportableBookmarks(bookmarks) {
|
||||
if (!this.securitySettings.privacyMode) {
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
return bookmarks.filter(bookmark => !this.isBookmarkPrivate(bookmark.id));
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
authenticateUser(password) {
|
||||
const hashedPassword = this.hashPassword(password);
|
||||
|
||||
if (hashedPassword === this.securitySettings.encryptionKey) {
|
||||
this.securitySession.isAuthenticated = true;
|
||||
this.securitySession.loginAttempts = 0;
|
||||
this.logAccess('successful_login');
|
||||
return true;
|
||||
} else {
|
||||
this.securitySession.loginAttempts++;
|
||||
this.logAccess('failed_login_attempt');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockManager = new MockBookmarkManager();
|
||||
|
||||
function displayResult(containerId, message, isPass) {
|
||||
const container = document.getElementById(containerId);
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = `test-result ${isPass ? 'test-pass' : 'test-fail'}`;
|
||||
resultDiv.textContent = message;
|
||||
container.appendChild(resultDiv);
|
||||
}
|
||||
|
||||
function testEncryption() {
|
||||
const container = document.getElementById('encryptionResults');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Enable encryption
|
||||
mockManager.securitySettings.encryptionEnabled = true;
|
||||
mockManager.securitySettings.encryptionKey = mockManager.hashPassword('testpassword123');
|
||||
|
||||
// Test encryption
|
||||
const originalBookmark = { id: '1', title: 'Secret Bookmark', url: 'https://secret.com' };
|
||||
const encryptedBookmark = mockManager.encryptBookmark(originalBookmark);
|
||||
|
||||
// Verify encryption worked
|
||||
const titleEncrypted = encryptedBookmark.title !== originalBookmark.title;
|
||||
const urlEncrypted = encryptedBookmark.url !== originalBookmark.url;
|
||||
const hasEncryptedFlag = encryptedBookmark.encrypted === true;
|
||||
|
||||
displayResult('encryptionResults',
|
||||
`Title encryption: ${titleEncrypted ? 'PASS' : 'FAIL'}`, titleEncrypted);
|
||||
displayResult('encryptionResults',
|
||||
`URL encryption: ${urlEncrypted ? 'PASS' : 'FAIL'}`, urlEncrypted);
|
||||
displayResult('encryptionResults',
|
||||
`Encrypted flag: ${hasEncryptedFlag ? 'PASS' : 'FAIL'}`, hasEncryptedFlag);
|
||||
|
||||
// Test decryption
|
||||
const decryptedBookmark = mockManager.decryptBookmark(encryptedBookmark);
|
||||
const titleDecrypted = decryptedBookmark.title === originalBookmark.title;
|
||||
const urlDecrypted = decryptedBookmark.url === originalBookmark.url;
|
||||
|
||||
displayResult('encryptionResults',
|
||||
`Title decryption: ${titleDecrypted ? 'PASS' : 'FAIL'}`, titleDecrypted);
|
||||
displayResult('encryptionResults',
|
||||
`URL decryption: ${urlDecrypted ? 'PASS' : 'FAIL'}`, urlDecrypted);
|
||||
|
||||
} catch (error) {
|
||||
displayResult('encryptionResults', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
function testPrivacyMode() {
|
||||
const container = document.getElementById('privacyResults');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Test privacy toggle
|
||||
mockManager.toggleBookmarkPrivacy('1');
|
||||
const isPrivate = mockManager.isBookmarkPrivate('1');
|
||||
displayResult('privacyResults',
|
||||
`Privacy toggle: ${isPrivate ? 'PASS' : 'FAIL'}`, isPrivate);
|
||||
|
||||
// Test privacy mode filtering
|
||||
mockManager.securitySettings.privacyMode = true;
|
||||
const exportableBookmarks = mockManager.getExportableBookmarks(mockManager.bookmarks);
|
||||
const filteredCorrectly = exportableBookmarks.length === 1 &&
|
||||
exportableBookmarks[0].id === '2';
|
||||
|
||||
displayResult('privacyResults',
|
||||
`Privacy filtering: ${filteredCorrectly ? 'PASS' : 'FAIL'}`, filteredCorrectly);
|
||||
|
||||
// Test with privacy mode disabled
|
||||
mockManager.securitySettings.privacyMode = false;
|
||||
const allBookmarks = mockManager.getExportableBookmarks(mockManager.bookmarks);
|
||||
const noFiltering = allBookmarks.length === 2;
|
||||
|
||||
displayResult('privacyResults',
|
||||
`No filtering when disabled: ${noFiltering ? 'PASS' : 'FAIL'}`, noFiltering);
|
||||
|
||||
} catch (error) {
|
||||
displayResult('privacyResults', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
function testAccessLogging() {
|
||||
const container = document.getElementById('loggingResults');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Clear existing logs
|
||||
mockManager.accessLog = [];
|
||||
|
||||
// Test logging enabled
|
||||
mockManager.securitySettings.accessLogging = true;
|
||||
mockManager.logAccess('test_action', { detail: 'test' });
|
||||
|
||||
const logCreated = mockManager.accessLog.length === 1;
|
||||
displayResult('loggingResults',
|
||||
`Log entry created: ${logCreated ? 'PASS' : 'FAIL'}`, logCreated);
|
||||
|
||||
const logHasCorrectAction = mockManager.accessLog[0].action === 'test_action';
|
||||
displayResult('loggingResults',
|
||||
`Log has correct action: ${logHasCorrectAction ? 'PASS' : 'FAIL'}`, logHasCorrectAction);
|
||||
|
||||
// Test logging disabled
|
||||
mockManager.securitySettings.accessLogging = false;
|
||||
mockManager.logAccess('should_not_log');
|
||||
|
||||
const noNewLog = mockManager.accessLog.length === 1;
|
||||
displayResult('loggingResults',
|
||||
`No logging when disabled: ${noNewLog ? 'PASS' : 'FAIL'}`, noNewLog);
|
||||
|
||||
} catch (error) {
|
||||
displayResult('loggingResults', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
function testPasswordProtection() {
|
||||
const container = document.getElementById('passwordResults');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Set up password protection
|
||||
const testPassword = 'securepassword123';
|
||||
mockManager.securitySettings.encryptionKey = mockManager.hashPassword(testPassword);
|
||||
mockManager.securitySession.isAuthenticated = false;
|
||||
|
||||
// Test correct password
|
||||
const correctAuth = mockManager.authenticateUser(testPassword);
|
||||
displayResult('passwordResults',
|
||||
`Correct password authentication: ${correctAuth ? 'PASS' : 'FAIL'}`, correctAuth);
|
||||
|
||||
const isAuthenticated = mockManager.securitySession.isAuthenticated;
|
||||
displayResult('passwordResults',
|
||||
`Session authenticated: ${isAuthenticated ? 'PASS' : 'FAIL'}`, isAuthenticated);
|
||||
|
||||
// Reset for wrong password test
|
||||
mockManager.securitySession.isAuthenticated = false;
|
||||
mockManager.securitySession.loginAttempts = 0;
|
||||
|
||||
// Test wrong password
|
||||
const wrongAuth = mockManager.authenticateUser('wrongpassword');
|
||||
displayResult('passwordResults',
|
||||
`Wrong password rejected: ${!wrongAuth ? 'PASS' : 'FAIL'}`, !wrongAuth);
|
||||
|
||||
const attemptsIncremented = mockManager.securitySession.loginAttempts === 1;
|
||||
displayResult('passwordResults',
|
||||
`Login attempts incremented: ${attemptsIncremented ? 'PASS' : 'FAIL'}`, attemptsIncremented);
|
||||
|
||||
} catch (error) {
|
||||
displayResult('passwordResults', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user