- 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
392 lines
15 KiB
HTML
392 lines
15 KiB
HTML
<!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> |