feat: implement secure browser notifications system

- Added SecureNotificationManager with cross-browser support (Chrome, Firefox, Safari, Edge)
- Integrated WebRTC message notifications with tab visibility detection
- Implemented XSS protection, URL validation, and rate limiting
- Notifications shown only when chat tab is inactive
- Enforced HTTPS and user gesture requirements
This commit is contained in:
lockbitchat
2025-10-15 19:58:28 -04:00
parent 5b5cc67fdc
commit b087adfecc
14 changed files with 1999 additions and 56 deletions

View File

@@ -280,9 +280,11 @@
toggleQrManualMode,
nextQrFrame,
prevQrFrame,
markAnswerCreated
markAnswerCreated,
notificationIntegrationRef
}) => {
const [mode, setMode] = React.useState('select');
const [notificationPermissionRequested, setNotificationPermissionRequested] = React.useState(false);
const resetToSelect = () => {
setMode('select');
@@ -296,6 +298,110 @@
const handleVerificationReject = () => {
onVerifyConnection(false);
};
// Request notification permission on first user interaction
const requestNotificationPermissionOnInteraction = async () => {
if (notificationPermissionRequested) {
return; // Already requested
}
try {
// Check if Notification API is supported
if (!('Notification' in window)) {
return;
}
// Check if we're in a secure context
if (!window.isSecureContext && window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
return;
}
// Check current permission status
const currentPermission = Notification.permission;
// Only request if permission is default (not granted or denied)
if (currentPermission === 'default') {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Initialize notification integration immediately
try {
if (window.NotificationIntegration && webrtcManagerRef.current) {
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
await integration.init();
// Store reference for cleanup
notificationIntegrationRef.current = integration;
}
} catch (error) {
// Handle error silently
}
// Send welcome notification
setTimeout(() => {
try {
const welcomeNotification = new Notification('SecureBit Chat', {
body: 'Notifications enabled! You will receive alerts for new messages.',
icon: '/logo/icon-192x192.png',
tag: 'welcome-notification'
});
welcomeNotification.onclick = () => {
welcomeNotification.close();
};
setTimeout(() => {
welcomeNotification.close();
}, 5000);
} catch (error) {
// Handle error silently
}
}, 1000);
}
} else if (currentPermission === 'granted') {
// Initialize notification integration immediately
try {
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
await integration.init();
// Store reference for cleanup
notificationIntegrationRef.current = integration;
}
} catch (error) {
// Handle error silently
}
// Test notification to confirm it works
setTimeout(() => {
try {
const testNotification = new Notification('SecureBit Chat', {
body: 'Notifications are working! You will receive alerts for new messages.',
icon: '/logo/icon-192x192.png',
tag: 'test-notification'
});
testNotification.onclick = () => {
testNotification.close();
};
setTimeout(() => {
testNotification.close();
}, 5000);
} catch (error) {
// Handle error silently
}
}, 1000);
}
setNotificationPermissionRequested(true);
} catch (error) {
// Handle error silently
}
};
if (showVerification) {
return React.createElement('div', {
@@ -346,7 +452,10 @@
// Create Connection
React.createElement('div', {
key: 'create',
onClick: () => setMode('create'),
onClick: () => {
requestNotificationPermissionOnInteraction();
setMode('create');
},
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
}, [
React.createElement('div', {
@@ -418,7 +527,10 @@
// Join Connection
React.createElement('div', {
key: 'join',
onClick: () => setMode('join'),
onClick: () => {
requestNotificationPermissionOnInteraction();
setMode('join');
},
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
}, [
React.createElement('div', {
@@ -1023,6 +1135,7 @@
};
};
const EnhancedChatInterface = ({
messages,
messageInput,
@@ -1453,6 +1566,7 @@
}, []);
const webrtcManagerRef = React.useRef(null);
const notificationIntegrationRef = React.useRef(null);
// Expose for modules/UI that run outside this closure (e.g., inline handlers)
// Safe because it's a ref object and we maintain it centrally here
window.webrtcManagerRef = webrtcManagerRef;
@@ -1785,6 +1899,20 @@
handleVerificationStateChange
);
// Initialize notification integration if permission was already granted
if (Notification.permission === 'granted' && window.NotificationIntegration && !notificationIntegrationRef.current) {
try {
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
integration.init().then(() => {
notificationIntegrationRef.current = integration;
}).catch((error) => {
// Handle error silently
});
} catch (error) {
// Handle error silently
}
}
handleMessage(' SecureBit.chat Enhanced Security Edition v4.3.120 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
const handleBeforeUnload = (event) => {
@@ -3083,11 +3211,47 @@
}
};
const handleVerifyConnection = (isValid) => {
const handleVerifyConnection = async (isValid) => {
if (isValid) {
webrtcManagerRef.current.confirmVerification();
// Mark local verification as confirmed
setLocalVerificationConfirmed(true);
// Initialize notification integration if permission was granted
try {
if (window.NotificationIntegration && webrtcManagerRef.current && !notificationIntegrationRef.current) {
const integration = new window.NotificationIntegration(webrtcManagerRef.current);
await integration.init();
// Store reference for cleanup
notificationIntegrationRef.current = integration;
// Check if permission was already granted
const status = integration.getStatus();
if (status.permission === 'granted') {
setMessages(prev => [...prev, {
message: '✓ Notifications enabled - you will receive alerts when the tab is inactive',
type: 'system',
id: Date.now(),
timestamp: Date.now()
}]);
} else {
setMessages(prev => [...prev, {
message: ' Notifications disabled - you can enable them using the button on the main page',
type: 'system',
id: Date.now(),
timestamp: Date.now()
}]);
}
} else if (notificationIntegrationRef.current) {
} else {
// Handle error silently
}
} catch (error) {
console.warn('Failed to initialize notifications:', error);
// Don't show error to user, notifications are optional
}
} else {
setMessages(prev => [...prev, {
message: ' Verification rejected. The connection is unsafe! Session reset..',
@@ -3218,6 +3382,12 @@
if (webrtcManagerRef.current) {
webrtcManagerRef.current.disconnect();
}
// Cleanup notification integration
if (notificationIntegrationRef.current) {
notificationIntegrationRef.current.cleanup();
notificationIntegrationRef.current = null;
}
// Clear all connection-related states
setKeyFingerprint('');
@@ -3476,7 +3646,8 @@
nextQrFrame: nextQrFrame,
prevQrFrame: prevQrFrame,
// PAKE passwords removed - using SAS verification instead
markAnswerCreated: markAnswerCreated
markAnswerCreated: markAnswerCreated,
notificationIntegrationRef: notificationIntegrationRef
})
),
@@ -3586,6 +3757,7 @@
if (!window.initializeApp) {
window.initializeApp = initializeApp;
}
}
};
// Render Enhanced Application
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));

View File

@@ -0,0 +1,279 @@
/**
* Notification Integration Module for SecureBit WebRTC Chat
* Integrates secure notifications with existing WebRTC architecture
*
* @version 1.0.0
* @author SecureBit Team
* @license MIT
*/
import { SecureChatNotificationManager } from './SecureNotificationManager.js';
class NotificationIntegration {
constructor(webrtcManager) {
this.webrtcManager = webrtcManager;
this.notificationManager = new SecureChatNotificationManager({
maxQueueSize: 10,
rateLimitMs: 1000, // Reduced from 2000ms to 1000ms
trustedOrigins: [
window.location.origin,
// Add other trusted origins for CDN icons
]
});
this.isInitialized = false;
this.originalOnMessage = null;
this.originalOnStatusChange = null;
this.processedMessages = new Set(); // Track processed messages to avoid duplicates
}
/**
* Initialize notification integration
* @returns {Promise<boolean>} Initialization success
*/
async init() {
try {
if (this.isInitialized) {
return true;
}
// Store original callbacks
this.originalOnMessage = this.webrtcManager.onMessage;
this.originalOnStatusChange = this.webrtcManager.onStatusChange;
// Wrap the original onMessage callback
this.webrtcManager.onMessage = (message, type) => {
this.handleIncomingMessage(message, type);
// Call original callback if it exists
if (this.originalOnMessage) {
this.originalOnMessage(message, type);
}
};
// Wrap the original onStatusChange callback
this.webrtcManager.onStatusChange = (status) => {
this.handleStatusChange(status);
// Call original callback if it exists
if (this.originalOnStatusChange) {
this.originalOnStatusChange(status);
}
};
// Also hook into the deliverMessageToUI method if it exists
if (this.webrtcManager.deliverMessageToUI) {
this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);
this.webrtcManager.deliverMessageToUI = (message, type) => {
this.handleIncomingMessage(message, type);
this.originalDeliverMessageToUI(message, type);
};
}
this.isInitialized = true;
return true;
} catch (error) {
return false;
}
}
/**
* Handle incoming messages and trigger notifications
* @param {*} message - Message content
* @param {string} type - Message type
* @private
*/
handleIncomingMessage(message, type) {
try {
// Create a unique key for this message to avoid duplicates
const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;
// Skip if we've already processed this message
if (this.processedMessages.has(messageKey)) {
return;
}
// Mark message as processed
this.processedMessages.add(messageKey);
// Clean up old processed messages (keep only last 100)
if (this.processedMessages.size > 100) {
const messagesArray = Array.from(this.processedMessages);
this.processedMessages.clear();
messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));
}
// Only process chat messages, not system messages
if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {
return;
}
// Extract message information
const messageInfo = this.extractMessageInfo(message, type);
if (!messageInfo) {
return;
}
// Send notification
const notificationResult = this.notificationManager.notify(
messageInfo.senderName,
messageInfo.text,
{
icon: messageInfo.senderAvatar,
senderId: messageInfo.senderId,
onClick: (senderId) => {
this.focusChatWindow();
}
}
);
} catch (error) {
// Handle error silently
}
}
/**
* Handle status changes
* @param {string} status - Connection status
* @private
*/
handleStatusChange(status) {
try {
// Clear notifications when connection is lost
if (status === 'disconnected' || status === 'failed') {
this.notificationManager.clearNotificationQueue();
this.notificationManager.resetUnreadCount();
}
} catch (error) {
// Handle error silently
}
}
/**
* Extract message information for notifications
* @param {*} message - Message content
* @param {string} type - Message type
* @returns {Object|null} Extracted message info or null
* @private
*/
extractMessageInfo(message, type) {
try {
let messageData = message;
// Handle different message formats
if (typeof message === 'string') {
try {
messageData = JSON.parse(message);
} catch (e) {
// Plain text message
return {
senderName: 'Peer',
text: message,
senderId: 'peer',
senderAvatar: null
};
}
}
// Handle structured message data
if (typeof messageData === 'object' && messageData !== null) {
return {
senderName: messageData.senderName || messageData.name || 'Peer',
text: messageData.text || messageData.message || messageData.content || '',
senderId: messageData.senderId || messageData.id || 'peer',
senderAvatar: messageData.senderAvatar || messageData.avatar || null
};
}
return null;
} catch (error) {
return null;
}
}
/**
* Focus chat window when notification is clicked
* @private
*/
focusChatWindow() {
try {
window.focus();
// Scroll to bottom of messages if container exists
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
} catch (error) {
// Handle error silently
}
}
/**
* Request notification permission
* @returns {Promise<boolean>} Permission granted status
*/
async requestPermission() {
try {
return await this.notificationManager.requestPermission();
} catch (error) {
return false;
}
}
/**
* Get notification status
* @returns {Object} Notification status
*/
getStatus() {
return this.notificationManager.getStatus();
}
/**
* Clear all notifications
*/
clearNotifications() {
this.notificationManager.clearNotificationQueue();
this.notificationManager.resetUnreadCount();
}
/**
* Cleanup integration
*/
cleanup() {
try {
if (this.isInitialized) {
// Restore original callbacks
if (this.originalOnMessage) {
this.webrtcManager.onMessage = this.originalOnMessage;
}
if (this.originalOnStatusChange) {
this.webrtcManager.onStatusChange = this.originalOnStatusChange;
}
if (this.originalDeliverMessageToUI) {
this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;
}
// Clear notifications
this.clearNotifications();
this.isInitialized = false;
}
} catch (error) {
// Handle error silently
}
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { NotificationIntegration };
}
// Global export for browser usage
if (typeof window !== 'undefined') {
window.NotificationIntegration = NotificationIntegration;
}

View File

@@ -0,0 +1,606 @@
/**
* Secure and Reliable Notification Manager for P2P WebRTC Chat
* Follows best practices: OWASP, MDN, Chrome DevRel
*
* @version 1.0.0
* @author SecureBit Team
* @license MIT
*/
class SecureChatNotificationManager {
constructor(config = {}) {
this.permission = Notification.permission;
this.isTabActive = this.checkTabActive(); // Initialize with proper check
this.unreadCount = 0;
this.originalTitle = document.title;
this.notificationQueue = [];
this.maxQueueSize = config.maxQueueSize || 5;
this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection
this.lastNotificationTime = 0;
this.trustedOrigins = config.trustedOrigins || [];
// Secure context flag
this.isSecureContext = window.isSecureContext;
// Cross-browser compatibility for Page Visibility API
this.hidden = this.getHiddenProperty();
this.visibilityChange = this.getVisibilityChangeEvent();
this.initVisibilityTracking();
this.initSecurityChecks();
}
/**
* Initialize security checks and validation
* @private
*/
initSecurityChecks() {
// Security checks are performed silently
}
/**
* Get hidden property name for cross-browser compatibility
* @returns {string} Hidden property name
* @private
*/
getHiddenProperty() {
if (typeof document.hidden !== "undefined") {
return "hidden";
} else if (typeof document.msHidden !== "undefined") {
return "msHidden";
} else if (typeof document.webkitHidden !== "undefined") {
return "webkitHidden";
}
return "hidden"; // fallback
}
/**
* Get visibility change event name for cross-browser compatibility
* @returns {string} Visibility change event name
* @private
*/
getVisibilityChangeEvent() {
if (typeof document.hidden !== "undefined") {
return "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
return "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
return "webkitvisibilitychange";
}
return "visibilitychange"; // fallback
}
/**
* Check if tab is currently active using multiple methods
* @returns {boolean} True if tab is active
* @private
*/
checkTabActive() {
// Primary method: Page Visibility API
if (this.hidden && typeof document[this.hidden] !== "undefined") {
return !document[this.hidden];
}
// Fallback method: document.hasFocus()
if (typeof document.hasFocus === "function") {
return document.hasFocus();
}
// Ultimate fallback: assume active
return true;
}
/**
* Initialize page visibility tracking (Page Visibility API)
* @private
*/
initVisibilityTracking() {
// Primary method: Page Visibility API with cross-browser support
if (typeof document.addEventListener !== "undefined" && typeof document[this.hidden] !== "undefined") {
document.addEventListener(this.visibilityChange, () => {
this.isTabActive = this.checkTabActive();
if (this.isTabActive) {
this.resetUnreadCount();
this.clearNotificationQueue();
}
});
}
// Fallback method: Window focus/blur events
window.addEventListener('focus', () => {
this.isTabActive = this.checkTabActive();
if (this.isTabActive) {
this.resetUnreadCount();
}
});
window.addEventListener('blur', () => {
this.isTabActive = this.checkTabActive();
});
// Page unload cleanup
window.addEventListener('beforeunload', () => {
this.clearNotificationQueue();
});
}
/**
* Request notification permission (BEST PRACTICE: Only call in response to user action)
* Never call on page load!
* @returns {Promise<boolean>} Permission granted status
*/
async requestPermission() {
// Secure context check
if (!this.isSecureContext || !('Notification' in window)) {
return false;
}
if (this.permission === 'granted') {
return true;
}
if (this.permission === 'denied') {
return false;
}
try {
this.permission = await Notification.requestPermission();
return this.permission === 'granted';
} catch (error) {
return false;
}
}
/**
* Update page title with unread count
* @private
*/
updateTitle() {
if (this.unreadCount > 0) {
document.title = `(${this.unreadCount}) ${this.originalTitle}`;
} else {
document.title = this.originalTitle;
}
}
/**
* XSS Protection: Sanitize input text
* @param {string} text - Text to sanitize
* @returns {string} Sanitized text
* @private
*/
sanitizeText(text) {
if (typeof text !== 'string') {
return '';
}
// Remove HTML tags and potentially dangerous characters
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.substring(0, 500); // Length limit
}
/**
* Validate icon URL (XSS protection)
* @param {string} url - URL to validate
* @returns {string|null} Validated URL or null
* @private
*/
validateIconUrl(url) {
if (!url) return null;
try {
const parsedUrl = new URL(url, window.location.origin);
// Only allow HTTPS and data URLs
if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {
// Check trusted origins if specified
if (this.trustedOrigins.length > 0) {
const isTrusted = this.trustedOrigins.some(origin =>
parsedUrl.origin === origin
);
return isTrusted ? parsedUrl.href : null;
}
return parsedUrl.href;
}
return null;
} catch (error) {
return null;
}
}
/**
* Rate limiting for spam protection
* @returns {boolean} Rate limit check passed
* @private
*/
checkRateLimit() {
const now = Date.now();
if (now - this.lastNotificationTime < this.rateLimitMs) {
return false;
}
this.lastNotificationTime = now;
return true;
}
/**
* Send secure notification
* @param {string} senderName - Name of message sender
* @param {string} message - Message content
* @param {Object} options - Notification options
* @returns {Notification|null} Created notification or null
*/
notify(senderName, message, options = {}) {
// Update tab active state before checking
this.isTabActive = this.checkTabActive();
// Only show if tab is NOT active (user is on another tab or minimized)
if (this.isTabActive) {
return null;
}
// Permission check
if (this.permission !== 'granted') {
return null;
}
// Rate limiting
if (!this.checkRateLimit()) {
return null;
}
// Data sanitization (XSS Protection)
const safeSenderName = this.sanitizeText(senderName || 'Unknown');
const safeMessage = this.sanitizeText(message || '');
const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';
// Queue overflow protection
if (this.notificationQueue.length >= this.maxQueueSize) {
this.clearNotificationQueue();
}
try {
const notification = new Notification(
`${safeSenderName}`,
{
body: safeMessage.substring(0, 200), // Length limit
icon: safeIcon,
badge: safeIcon,
tag: `chat-${options.senderId || 'unknown'}`, // Grouping
requireInteraction: false, // Don't block user
silent: options.silent || false,
// Vibrate only for mobile and if supported
vibrate: navigator.vibrate ? [200, 100, 200] : undefined,
// Safe metadata
data: {
senderId: this.sanitizeText(options.senderId),
timestamp: Date.now(),
// Don't include sensitive data!
}
}
);
// Increment counter
this.unreadCount++;
this.updateTitle();
// Add to queue for management
this.notificationQueue.push(notification);
// Safe click handler
notification.onclick = (event) => {
event.preventDefault(); // Prevent default behavior
window.focus();
notification.close();
// Safe callback
if (typeof options.onClick === 'function') {
try {
options.onClick(options.senderId);
} catch (error) {
console.error('[Notifications] Error in onClick handler:', error);
}
}
};
// Error handler
notification.onerror = (event) => {
console.error('[Notifications] Error showing notification:', event);
};
// Auto-close after reasonable time
const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);
setTimeout(() => {
notification.close();
this.removeFromQueue(notification);
}, autoCloseTimeout);
return notification;
} catch (error) {
console.error('[Notifications] Failed to create notification:', error);
return null;
}
}
/**
* Remove notification from queue
* @param {Notification} notification - Notification to remove
* @private
*/
removeFromQueue(notification) {
const index = this.notificationQueue.indexOf(notification);
if (index > -1) {
this.notificationQueue.splice(index, 1);
}
}
/**
* Clear all notifications
*/
clearNotificationQueue() {
this.notificationQueue.forEach(notification => {
try {
notification.close();
} catch (error) {
// Ignore errors when closing
}
});
this.notificationQueue = [];
}
/**
* Reset unread counter
*/
resetUnreadCount() {
this.unreadCount = 0;
this.updateTitle();
}
/**
* Get current status
* @returns {Object} Current notification status
*/
getStatus() {
return {
permission: this.permission,
isTabActive: this.isTabActive,
unreadCount: this.unreadCount,
isSecureContext: this.isSecureContext,
queueSize: this.notificationQueue.length
};
}
}
/**
* Secure integration with WebRTC
*/
class SecureP2PChat {
constructor() {
this.notificationManager = new SecureChatNotificationManager({
maxQueueSize: 5,
rateLimitMs: 2000,
trustedOrigins: [
window.location.origin,
// Add other trusted origins for CDN icons
]
});
this.dataChannel = null;
this.peerConnection = null;
this.remotePeerName = 'Peer';
this.messageHistory = [];
this.maxHistorySize = 100;
}
/**
* Initialize when user connects
*/
async init() {
// Initialize notification manager silently
}
/**
* Method for manual permission request (called on click)
* @returns {Promise<boolean>} Permission granted status
*/
async enableNotifications() {
const granted = await this.notificationManager.requestPermission();
return granted;
}
/**
* Setup DataChannel with security checks
* @param {RTCDataChannel} dataChannel - WebRTC data channel
*/
setupDataChannel(dataChannel) {
if (!dataChannel) {
console.error('[Chat] Invalid DataChannel');
return;
}
this.dataChannel = dataChannel;
// Setup handlers
this.dataChannel.onmessage = (event) => {
this.handleIncomingMessage(event.data);
};
this.dataChannel.onerror = (error) => {
// Handle error silently
};
}
/**
* XSS Protection: Validate incoming messages
* @param {string|Object} data - Message data
* @returns {Object|null} Validated message or null
* @private
*/
validateMessage(data) {
try {
const message = typeof data === 'string' ? JSON.parse(data) : data;
// Check message structure
if (!message || typeof message !== 'object') {
throw new Error('Invalid message structure');
}
// Check required fields
if (!message.text || typeof message.text !== 'string') {
throw new Error('Invalid message text');
}
// Message length limit (DoS protection)
if (message.text.length > 10000) {
throw new Error('Message too long');
}
return {
text: message.text,
senderName: message.senderName || 'Unknown',
senderId: message.senderId || 'unknown',
timestamp: message.timestamp || Date.now(),
senderAvatar: message.senderAvatar || null
};
} catch (error) {
console.error('[Chat] Message validation failed:', error);
return null;
}
}
/**
* Secure handling of incoming messages
* @param {string|Object} data - Message data
* @private
*/
handleIncomingMessage(data) {
const message = this.validateMessage(data);
if (!message) {
return;
}
// Save to history (with limit)
this.messageHistory.push(message);
if (this.messageHistory.length > this.maxHistorySize) {
this.messageHistory.shift();
}
// Display in UI (with sanitization)
this.displayMessage(message);
// Send notification only if tab is inactive
this.notificationManager.notify(
message.senderName,
message.text,
{
icon: message.senderAvatar,
senderId: message.senderId,
onClick: (senderId) => {
this.scrollToLatestMessage();
}
}
);
// Optional: sound (with check)
if (!this.notificationManager.isTabActive) {
this.playNotificationSound();
}
}
/**
* XSS Protection: Safe message display
* @param {Object} message - Message to display
* @private
*/
displayMessage(message) {
const container = document.getElementById('messages');
if (!container) {
return;
}
const messageEl = document.createElement('div');
messageEl.className = 'message';
// Use textContent to prevent XSS
const nameEl = document.createElement('strong');
nameEl.textContent = message.senderName + ': ';
const textEl = document.createElement('span');
textEl.textContent = message.text;
const timeEl = document.createElement('small');
timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();
messageEl.appendChild(nameEl);
messageEl.appendChild(textEl);
messageEl.appendChild(document.createElement('br'));
messageEl.appendChild(timeEl);
container.appendChild(messageEl);
this.scrollToLatestMessage();
}
/**
* Safe sound playback
* @private
*/
playNotificationSound() {
try {
// Use only local audio files
const audio = new Audio('/assets/audio/notification.mp3');
audio.volume = 0.3; // Moderate volume
// Error handling
audio.play().catch(error => {
// Handle audio error silently
});
} catch (error) {
// Handle audio creation error silently
}
}
/**
* Scroll to latest message
* @private
*/
scrollToLatestMessage() {
const container = document.getElementById('messages');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
/**
* Get status
* @returns {Object} Current chat status
*/
getStatus() {
return {
notifications: this.notificationManager.getStatus(),
messageCount: this.messageHistory.length,
connected: this.dataChannel?.readyState === 'open'
};
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SecureChatNotificationManager, SecureP2PChat };
}
// Global export for browser usage
if (typeof window !== 'undefined') {
window.SecureChatNotificationManager = SecureChatNotificationManager;
window.SecureP2PChat = SecureP2PChat;
}

View File

@@ -96,7 +96,7 @@ class PWAInstallPrompt {
setupEventListeners() {
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
// Don't prevent default - let browser show its own banner
this.deferredPrompt = event;
if (this.checkInstallationStatus()) {

View File

@@ -1,6 +1,7 @@
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
import { NotificationIntegration } from '../notifications/NotificationIntegration.js';
// Import UI components (side-effect: they attach themselves to window.*)
import '../components/ui/Header.jsx';
@@ -16,6 +17,7 @@ import '../components/ui/FileTransfer.jsx';
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
window.NotificationIntegration = NotificationIntegration;
// Mount application once DOM and modules are ready
const start = () => {

View File

@@ -3,10 +3,13 @@
(async () => {
try {
const timestamp = Date.now();
const [cryptoModule, webrtcModule, fileTransferModule] = await Promise.all([
const [cryptoModule, webrtcModule, fileTransferModule, notificationModule, notificationTestModule, notificationGestureTestModule] = await Promise.all([
import(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
import(`../transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`),
import(`../notifications/NotificationIntegration.js?v=${timestamp}`),
import(`../notifications/NotificationTest.js?v=${timestamp}`),
import(`../notifications/NotificationGestureTest.js?v=${timestamp}`),
]);
const { EnhancedSecureCryptoUtils } = cryptoModule;
@@ -15,6 +18,12 @@
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
const { EnhancedSecureFileTransfer } = fileTransferModule;
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
const { NotificationIntegration } = notificationModule;
window.NotificationIntegration = NotificationIntegration;
const { NotificationTest } = notificationTestModule;
window.NotificationTest = NotificationTest;
const { NotificationGestureTest } = notificationGestureTestModule;
window.NotificationGestureTest = NotificationGestureTest;
// Load React components using dynamic imports instead of eval
const componentModules = await Promise.all([