diff --git a/Backend/routes/auth.js b/Backend/routes/auth.js index 1fbd536..e0aaaca 100644 --- a/Backend/routes/auth.js +++ b/Backend/routes/auth.js @@ -50,7 +50,7 @@ router.post('/login/verify', async (req, res) => { try { const result = await db.query( - 'SELECT hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1', + 'SELECT id, hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1', [username] ); diff --git a/Frontend/Electron/main.cjs b/Frontend/Electron/main.cjs new file mode 100644 index 0000000..3fdfa25 --- /dev/null +++ b/Frontend/Electron/main.cjs @@ -0,0 +1,162 @@ +const { app, BrowserWindow, ipcMain, shell } = require('electron'); +const path = require('path'); +const https = require('https'); +const http = require('http'); + +function createWindow() { + const win = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.cjs'), + sandbox: false + } + }); + + const startUrl = process.env.ELECTRON_START_URL || `file://${path.join(__dirname, '../dist/index.html')}`; + + if (process.env.npm_lifecycle_event === 'electron:dev') { + win.loadURL('http://localhost:5173'); + win.webContents.openDevTools(); + } else { + win.loadURL(startUrl); + } +} + +app.whenReady().then(() => { + createWindow(); + + // Helper to fetch metadata (Zero-Knowledge: Client fetches previews) + ipcMain.handle('fetch-metadata', async (event, url) => { + return new Promise((resolve) => { + const client = url.startsWith('https') ? https : http; + const req = client.get(url, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + // Simple Regex Parser for OG Tags to avoid dependencies + const getMeta = (prop) => { + const regex = new RegExp(` { + const regex = /(.*?)<\/title>/i; + const match = data.match(regex); + return match ? match[1] : null; + }; + + const metadata = { + title: getMeta('title') || getTitle(), + description: getMeta('description'), + image: getMeta('image'), + siteName: getMeta('site_name'), + themeColor: getMeta('theme-color') + }; + resolve(metadata); + }); + }); + req.on('error', (err) => { + console.error('Metadata fetch error:', err); + resolve(null); + }); + }); + }); + + ipcMain.handle('open-external', async (event, url) => { + await shell.openExternal(url); + }); + + // Crypto Handlers + const crypto = require('crypto'); + + ipcMain.handle('generate-keys', async () => { + const generateRSA = () => new Promise((resolve, reject) => { + crypto.generateKeyPair('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }, (err, pub, priv) => { /* args are (err, publicKey, privateKey) */ + if (err) reject(err); else resolve({ pub, priv }); + }); + }); + + const generateEd = () => new Promise((resolve, reject) => { + crypto.generateKeyPair('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }, (err, pub, priv) => { + if (err) reject(err); else resolve({ pub, priv }); + }); + }); + + const [rsa, ed] = await Promise.all([generateRSA(), generateEd()]); + + return { + rsaPub: rsa.pub, + rsaPriv: rsa.priv, + edPub: ed.pub, + edPriv: ed.priv + }; + }); + + ipcMain.handle('random-bytes', (event, size) => { + return crypto.randomBytes(size).toString('hex'); + }); + + ipcMain.handle('sha256', (event, data) => { + return crypto.createHash('sha256').update(data).digest('hex'); + }); + + ipcMain.handle('derive-auth-keys', (event, password, salt) => { + return new Promise((resolve, reject) => { + crypto.scrypt(password, salt, 64, (err, derivedKey) => { + if (err) reject(err); + else { + const dak = derivedKey.subarray(0, 32).toString('hex'); + const dek = derivedKey.subarray(32, 64); + resolve({ dak, dek }); + } + }); + }); + }); + + ipcMain.handle('encrypt-data', (event, plaintext, key) => { + console.log('encrypt-data called with:', { + plaintextType: typeof plaintext, + isBuffer: Buffer.isBuffer(plaintext), + keyType: typeof key, + keyIsBuffer: Buffer.isBuffer(plaintext) + }); + + if (plaintext === undefined) throw new TypeError('plaintext is undefined'); + if (key === undefined) throw new TypeError('key is undefined'); + + // Key can be hex string or buffer. 32 bytes for AES-256 + const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key; + const iv = crypto.randomBytes(12); // 96-bit IV for GCM + const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv); + + // Use Buffer concatenation to handle both string and Buffer inputs correctly without encoding confusion + const encryptedBuffer = Buffer.concat([ + cipher.update(plaintext), + cipher.final() + ]); + + const tag = cipher.getAuthTag().toString('hex'); + return { content: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag }; + }); + + ipcMain.handle('decrypt-data', (event, ciphertext, key, iv, tag) => { + const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'hex') : key; + const ivBuffer = Buffer.from(iv, 'hex'); + const tagBuffer = Buffer.from(tag, 'hex'); + const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer); + decipher.setAuthTag(tagBuffer); + let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + }); +}); diff --git a/Frontend/Electron/main.js b/Frontend/Electron/main.js deleted file mode 100644 index ceadf1f..0000000 --- a/Frontend/Electron/main.js +++ /dev/null @@ -1,101 +0,0 @@ -import { app, BrowserWindow, ipcMain } from 'electron'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import crypto from 'node:crypto'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -function createWindow() { - const win = new BrowserWindow({ - width: 1200, - height: 800, - webPreferences: { - preload: path.join(__dirname, 'preload.cjs'), - contextIsolation: true, - sandbox: true, - nodeIntegration: false, - }, - }); - - if (process.env.NODE_ENV === 'development' || process.argv.includes('--dev')) { - win.loadURL('http://localhost:5173'); - win.webContents.openDevTools(); - } else { - win.loadFile(path.join(__dirname, 'dist', 'index.html')); - } -} - -app.whenReady().then(() => { - // Crypto IPC Handlers - ipcMain.handle('crypto:deriveAuthKeys', async (_, password, salt) => { - return new Promise((resolve, reject) => { - crypto.pbkdf2(password, salt, 100000, 32, 'sha512', (err, derived) => { - if (err) reject(err); - resolve({ - dek: derived.slice(0, 16).toString('hex'), - dak: derived.slice(16, 32).toString('hex') - }); - }); - }); - }); - - ipcMain.handle('crypto:encryptData', (_, plaintext, keyHex, ivHex) => { - const key = Buffer.from(keyHex, 'hex'); - const iv = ivHex ? Buffer.from(ivHex, 'hex') : crypto.randomBytes(12); - const cipher = crypto.createCipheriv('aes-128-gcm', key, iv); - let encrypted = cipher.update(plaintext, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return { - content: encrypted, - tag: cipher.getAuthTag().toString('hex'), - iv: iv.toString('hex') - }; - }); - - ipcMain.handle('crypto:decryptData', (_, ciphertext, keyHex, ivHex, tagHex) => { - const key = Buffer.from(keyHex, 'hex'); - const iv = Buffer.from(ivHex, 'hex'); - const tag = Buffer.from(tagHex, 'hex'); - const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv); - decipher.setAuthTag(tag); - let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; - }); - - ipcMain.handle('crypto:generateKeys', async () => { - const { publicKey: rsaPub, privateKey: rsaPriv } = crypto.generateKeyPairSync('rsa', { - modulusLength: 2048, - publicKeyEncoding: { type: 'spki', format: 'pem' }, - privateKeyEncoding: { type: 'pkcs8', format: 'pem' } - }); - const { publicKey: edPub, privateKey: edPriv } = crypto.generateKeyPairSync('ed25519', { - publicKeyEncoding: { type: 'spki', format: 'pem' }, - privateKeyEncoding: { type: 'pkcs8', format: 'pem' } - }); - return { rsaPub, rsaPriv, edPub, edPriv }; - }); - - ipcMain.handle('crypto:randomBytes', (_, size) => { - return crypto.randomBytes(size).toString('hex'); - }); - - ipcMain.handle('crypto:sha256', (_, data) => { - return crypto.createHash('sha256').update(data).digest('hex'); - }); - - createWindow(); - - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json index de0766e..cf84f8f 100644 --- a/Frontend/Electron/package.json +++ b/Frontend/Electron/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", - "main": "main.js", + "main": "main.cjs", "homepage": "./", "scripts": { "dev": "vite", @@ -37,4 +37,4 @@ "vite": "^7.2.4", "wait-on": "^8.0.1" } -} +} \ No newline at end of file diff --git a/Frontend/Electron/preload.cjs b/Frontend/Electron/preload.cjs index ddf4383..9ee21fc 100644 --- a/Frontend/Electron/preload.cjs +++ b/Frontend/Electron/preload.cjs @@ -1,10 +1,12 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('cryptoAPI', { - deriveAuthKeys: (password, salt) => ipcRenderer.invoke('crypto:deriveAuthKeys', password, salt), - encryptData: (plaintext, keyHex, ivHex) => ipcRenderer.invoke('crypto:encryptData', plaintext, keyHex, ivHex), - decryptData: (ciphertext, keyHex, ivHex, tagHex) => ipcRenderer.invoke('crypto:decryptData', ciphertext, keyHex, ivHex, tagHex), - generateKeys: () => ipcRenderer.invoke('crypto:generateKeys'), - randomBytes: (size) => ipcRenderer.invoke('crypto:randomBytes', size), - sha256: (data) => ipcRenderer.invoke('crypto:sha256', data) + generateKeys: () => ipcRenderer.invoke('generate-keys'), + randomBytes: (size) => ipcRenderer.invoke('random-bytes', size), + sha256: (data) => ipcRenderer.invoke('sha256', data), + deriveAuthKeys: (password, salt) => ipcRenderer.invoke('derive-auth-keys', password, salt), + encryptData: (data, key) => ipcRenderer.invoke('encrypt-data', data, key), + decryptData: (encryptedData, key, iv, tag) => ipcRenderer.invoke('decrypt-data', encryptedData, key, iv, tag), + fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url), + openExternal: (url) => ipcRenderer.invoke('open-external', url), }); diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index 5ca4a43..4859621 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -10,20 +10,26 @@ const ChatArea = ({ channelId, username }) => { const [input, setInput] = useState(''); const [socket, setSocket] = useState(null); const messagesEndRef = useRef(null); + const textareaRef = useRef(null); - // Mock Key for demo (In real app, derive from Channel Key Bundle) - const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f'; + // Mock Key for demo (32 bytes hex = 64 chars) + const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f'; + + // Helper to get consistent color for user + const getUserColor = (username) => { + const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; + let hash = 0; + for (let i = 0; i < username.length; i++) { + hash = username.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; + }; // Helper to decrypt message const decryptMessage = async (msg) => { try { - // Check if ciphertext has appended tag - // Tag is 16 bytes = 32 hex chars const TAG_LENGTH = 32; if (!msg.ciphertext || msg.ciphertext.length < TAG_LENGTH) { - // Try decrypting without tag if it was legacy (though we just started) - // Or maybe it's just raw text if not encrypted? No, we always encrypt. - console.warn('Message missing tag, trying raw decrypt or fail'); return '[Invalid Encrypted Message]'; } @@ -38,6 +44,85 @@ const ChatArea = ({ channelId, username }) => { } }; + // Helper to extract URLs + const extractUrls = (text) => { + const urlRegex = /(https?:\/\/[^\s]+)/g; + return text.match(urlRegex) || []; + }; + + const LinkPreview = ({ url }) => { + const [metadata, setMetadata] = useState(null); + const [loading, setLoading] = useState(true); + const [playing, setPlaying] = useState(false); + + useEffect(() => { + let isMounted = true; + const fetchMeta = async () => { + try { + const data = await window.cryptoAPI.fetchMetadata(url); + if (isMounted) { + setMetadata(data); + setLoading(false); + } + } catch (err) { + console.error("Failed to fetch metadata", err); + if (isMounted) setLoading(false); + } + }; + fetchMeta(); + return () => { isMounted = false; }; + }, [url]); + + // Helper to extract video ID + const getYouTubeId = (link) => { + const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + const match = link.match(regExp); + return (match && match[2].length === 11) ? match[2] : null; + }; + + const videoId = getYouTubeId(url); + const isYouTube = !!videoId; + + if (loading || !metadata || (!metadata.title && !metadata.image)) return null; + + return ( + <div className={`link-preview ${isYouTube ? 'youtube-preview' : ''}`} style={{ borderLeftColor: metadata.themeColor || '#202225' }}> + <div className="preview-content"> + {metadata.siteName && <div className="preview-site-name">{metadata.siteName}</div>} + {metadata.title && ( + <a href={url} onClick={(e) => { e.preventDefault(); window.cryptoAPI.openExternal(url); }} className="preview-title"> + {metadata.title} + </a> + )} + {metadata.description && <div className="preview-description">{metadata.description}</div>} + + {isYouTube && playing && ( + <div className="youtube-video-wrapper"> + <iframe + src={`https://www.youtube.com/embed/${videoId}?autoplay=1`} + title={metadata.title || "YouTube video player"} + frameBorder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" + allowFullScreen + /> + </div> + )} + </div> + + {metadata.image && (!isYouTube || !playing) && ( + <div + className="preview-image-container" + onClick={() => isYouTube && setPlaying(true)} + style={isYouTube ? { cursor: 'pointer' } : {}} + > + <img src={metadata.image} alt="Preview" className="preview-image" /> + {isYouTube && <div className="play-icon">▶</div>} + </div> + )} + </div> + ); + }; + useEffect(() => { const newSocket = io('http://localhost:3000'); setSocket(newSocket); @@ -64,32 +149,37 @@ const ChatArea = ({ channelId, username }) => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`; + } + }, [input]); + const handleSend = async (e) => { e.preventDefault(); if (!input.trim()) return; try { - // Encrypt message const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY); - - // Append tag to ciphertext for storage const ciphertext = encryptedContent + tag; - - // Sign message (placeholder) const signature = 'placeholder_signature'; + const senderId = localStorage.getItem('userId'); + if (!senderId) { + console.error('No userId found in localStorage'); + return; + } + const messageData = { channelId, - senderId: '8b105be1-981e-4200-bb07-68d0714870c2', // Placeholder default, gets overwritten below + senderId, ciphertext, nonce: iv, signature, keyVersion: 1 }; - const storedUserId = localStorage.getItem('userId'); - if (storedUserId) messageData.senderId = storedUserId; - socket.emit('send_message', messageData); setInput(''); } catch (err) { @@ -107,51 +197,114 @@ const ChatArea = ({ channelId, username }) => { return ( <div className="chat-area"> <div className="messages-list"> - {messages.map((msg, idx) => ( - <div key={idx} className="message-item"> - <div className="message-header"> - <span className="username">{msg.username || 'Unknown'}</span> - <span className="timestamp">{new Date(msg.created_at).toLocaleTimeString()}</span> + {messages.map((msg, idx) => { + const urls = extractUrls(msg.content); + return ( + <div key={idx} className="message-item"> + <div className="message-avatar-wrapper"> + <div + className="message-avatar" + style={{ backgroundColor: getUserColor(msg.username || 'Unknown') }} + > + {(msg.username || '?').substring(0, 1).toUpperCase()} + </div> + </div> + <div className="message-body"> + <div className="message-header"> + <span className="username" style={{ color: getUserColor(msg.username || 'Unknown') }}> + {msg.username || 'Unknown'} + </span> + <span className="timestamp"> + {new Date(msg.created_at).toLocaleDateString()} at {new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + </span> + </div> + <div className="message-content"> + <ReactMarkdown + remarkPlugins={[remarkGfm]} + components={{ + a: ({ node, ...props }) => ( + <a + {...props} + onClick={(e) => { + e.preventDefault(); + window.cryptoAPI.openExternal(props.href); + }} + style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }} + onMouseOver={(e) => e.target.style.textDecoration = 'underline'} + onMouseOut={(e) => e.target.style.textDecoration = 'none'} + /> + ), + code({ node, inline, className, children, ...props }) { + const match = /language-(\w+)/.exec(className || '') + return !inline && match ? ( + <SyntaxHighlighter + style={oneDark} + language={match[1]} + PreTag="div" + {...props} + > + {String(children).replace(/\n$/, '')} + </SyntaxHighlighter> + ) : ( + <code className={className} {...props}> + {children} + </code> + ) + }, + p: ({ node, ...props }) => <p style={{ margin: '0 0 6px 0' }} {...props} />, + h1: ({ node, ...props }) => <h1 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '12px 0 6px 0', lineHeight: 1.1 }} {...props} />, + h2: ({ node, ...props }) => <h2 style={{ fontSize: '1.25rem', fontWeight: 700, margin: '10px 0 6px 0', lineHeight: 1.1 }} {...props} />, + h3: ({ node, ...props }) => <h3 style={{ fontSize: '1.1rem', fontWeight: 700, margin: '8px 0 6px 0', lineHeight: 1.1 }} {...props} />, + ul: ({ node, ...props }) => <ul style={{ margin: '0 0 6px 0', paddingLeft: '20px' }} {...props} />, + ol: ({ node, ...props }) => <ol style={{ margin: '0 0 6px 0', paddingLeft: '20px' }} {...props} />, + li: ({ node, ...props }) => <li style={{ margin: '0' }} {...props} />, + hr: ({ node, ...props }) => <hr style={{ border: 'none', borderTop: '1px solid #4f545c', margin: '12px 0' }} {...props} />, + }} + > + {msg.content} + </ReactMarkdown> + {urls.map((url, i) => ( + <LinkPreview key={i} url={url} /> + ))} + </div> + </div> </div> - <div className="message-content"> - <ReactMarkdown - pluginPlugins={[remarkGfm]} - components={{ - code({ node, inline, className, children, ...props }) { - const match = /language-(\w+)/.exec(className || '') - return !inline && match ? ( - <SyntaxHighlighter - style={oneDark} - language={match[1]} - PreTag="div" - {...props} - > - {String(children).replace(/\n$/, '')} - </SyntaxHighlighter> - ) : ( - <code className={className} {...props}> - {children} - </code> - ) - } - }} - > - {msg.content} - </ReactMarkdown> - </div> - </div> - ))} + ); + })} <div ref={messagesEndRef} /> </div> <form className="chat-input-form" onSubmit={handleSend}> - <textarea - value={input} - onChange={(e) => setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={`Message #${channelId}`} - rows={1} - style={{ resize: 'none' }} // Disable manual resize - /> + <div className="chat-input-wrapper"> + <button type="button" className="chat-input-file-btn"> + <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2Z"></path> + </svg> + </button> + <textarea + ref={textareaRef} + value={input} + onChange={(e) => setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={`Message #${channelId}`} + rows={1} + style={{ height: '44px' }} + /> + <div className="chat-input-icons"> + <button type="button" className="chat-input-icon-btn"> + <span style={{ fontWeight: 700, fontSize: '12px', border: '2px solid currentColor', borderRadius: '4px', padding: '0 2px' }}>GIF</span> + </button> + <button type="button" className="chat-input-icon-btn"> + <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm11.8 13.9a5.5 5.5 0 0 1-9.14 0 1 1 0 0 1 .27-1.4 1 1 0 0 1 1.4.27 3.5 3.5 0 0 0 5.8 0 1 1 0 0 1 1.67 1.13Z"></path> + </svg> + </button> + <button type="button" className="chat-input-icon-btn"> + <svg aria-hidden="true" role="img" width="24" height="24" viewBox="0 0 24 24"> + <path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Zm-5.5-2.5a5.5 5.5 0 0 1 9 0 .5.5 0 0 1-.8.6 4.5 4.5 0 0 0-7.4 0 .5.5 0 0 1-.8-.6ZM8.5 10a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm5.5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Z"></path> + </svg> + </button> + </div> + </div> </form> </div> ); diff --git a/Frontend/Electron/src/index.css b/Frontend/Electron/src/index.css index bd2c450..9379193 100644 --- a/Frontend/Electron/src/index.css +++ b/Frontend/Electron/src/index.css @@ -31,7 +31,6 @@ body { justify-content: center; height: 100vh; background-image: url('https://discord.com/assets/f9e794909795f472.svg'); - /* Placeholder background */ background-size: cover; background-position: center; } @@ -122,6 +121,7 @@ body { /* Sidebar */ .sidebar { width: 300px; + min-width: 300px; background-color: var(--bg-secondary); display: flex; flex-direction: row; @@ -201,28 +201,71 @@ body { background-color: var(--bg-primary); display: flex; flex-direction: column; + position: relative; } .messages-list { flex: 1; overflow-y: auto; - padding: 20px; + padding: 0 0 20px 0; + display: flex; + flex-direction: column; +} + +.messages-list::-webkit-scrollbar { + width: 8px; + background-color: #2b2d31; +} + +.messages-list::-webkit-scrollbar-thumb { + background-color: #1a1b1e; + border-radius: 4px; } .message-item { - margin-bottom: 20px; + display: flex; + padding: 2px 16px; + margin-top: 17px; +} + +.message-item:hover { + background-color: rgba(2, 2, 2, 0.06); +} + +.message-avatar-wrapper { + width: 40px; + margin-right: 16px; + margin-top: 2px; +} + +.message-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + color: white; + font-size: 18px; + user-select: none; +} + +.message-body { + flex: 1; + min-width: 0; } .message-header { display: flex; - align-items: baseline; - margin-bottom: 4px; + align-items: center; + margin-bottom: 2px; } .username { - color: var(--header-primary); + font-size: 16px; font-weight: 500; - margin-right: 8px; + margin-right: 0.25rem; cursor: pointer; } @@ -232,54 +275,267 @@ body { .timestamp { color: var(--text-muted); - font-size: 12px; + font-size: 0.75rem; + margin-left: 0.25rem; + font-weight: 400; } .message-content { color: var(--text-normal); + font-size: 1rem; + line-height: 1.375rem; white-space: pre-wrap; word-wrap: break-word; } -.chat-input-form { - padding: 0 16px 24px; +/* Markdown Styles Tweaks */ +.message-content strong { + font-weight: 700; } -.chat-input-form textarea { - width: 100%; - padding: 11px 16px; - background-color: #40444b; - border: none; - border-radius: 8px; - color: var(--text-normal); - font-size: 16px; - font-family: inherit; - height: auto; - min-height: 44px; +.message-content h1, +.message-content h2, +.message-content h3 { + border-bottom: none; + font-weight: 700; + color: var(--header-primary); } -.chat-input-form textarea:focus { - outline: none; +.message-content ul, +.message-content ol { + list-style-type: disc; } -/* Markdown Styles */ -.message-content p { - margin: 0; +.message-content a { + color: #00b0f4; + text-decoration: none; +} + +.message-content a:hover { + text-decoration: underline; +} + +.message-content blockquote { + border-left: 4px solid #4f545c; + margin: 4px 0 4px 0; + padding: 0 8px 0 12px; + color: #b9bbbe; + background-color: transparent; } .message-content code { background-color: #2f3136; padding: 2px 4px; border-radius: 3px; - font-family: monospace; + font-family: Consolas, 'Courier New', monospace; + font-size: 0.875rem; } .message-content pre { - margin: 6px 0; + background-color: #2f3136 !important; + border-radius: 4px; + border: 1px solid #202225; + padding: 8px !important; + margin: 8px 0 !important; + max-width: 100%; + overflow-x: auto; } -.message-content blockquote { - border-left: 4px solid var(--interactive-normal); - margin: 0; - padding-left: 10px; +.chat-input-form { + padding: 0 16px 24px; + margin-top: 8px; + background-color: var(--bg-primary); +} + +.chat-input-wrapper { + background-color: #40444b; + border-radius: 8px; + display: flex; + align-items: center; + padding: 0 16px; + min-height: 44px; +} + +.chat-input-file-btn { + background: none; + border: none; + color: #b9bbbe; + cursor: pointer; + padding: 0 16px 0 0; + display: flex; + align-items: center; + height: 24px; + align-self: center; +} + +.chat-input-file-btn:hover { + color: var(--text-normal); +} + +.chat-input-form textarea { + flex: 1; + padding: 11px 0; + background-color: transparent; + border: none; + color: var(--text-normal); + font-size: 16px; + font-family: inherit; + height: 44px; + resize: none; + overflow: hidden; + line-height: 1.375rem; + box-sizing: border-box; +} + +.chat-input-form textarea:focus { + outline: none; +} + +.chat-input-form textarea::placeholder { + color: #72767d; +} + +.chat-input-icons { + display: flex; + align-items: center; + padding: 0 0 0 10px; + align-self: center; + height: 100%; +} + +.chat-input-icon-btn { + background: none; + border: none; + color: #b9bbbe; + cursor: pointer; + padding: 4px; + margin-left: 4px; + display: flex; + align-items: center; +} + +.chat-input-icon-btn:hover { + color: var(--text-normal); +} + +/* Link Previews */ +.link-preview { + display: flex; + background-color: #2f3136; + border-left: 4px solid #202225; + border-radius: 4px; + padding: 10px; + margin-top: 8px; + max-width: 520px; + gap: 16px; +} + +.preview-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; +} + +.preview-site-name { + font-size: 12px; + color: #b9bbbe; + margin-bottom: 4px; +} + +.preview-title { + font-size: 16px; + font-weight: 600; + color: #00b0f4; + text-decoration: none; + margin-bottom: 4px; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.preview-title:hover { + text-decoration: underline; +} + +.preview-description { + font-size: 14px; + color: #dcddde; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.preview-image-container { + flex-shrink: 0; + position: relative; +} + +.preview-image { + max-width: 150px; + max-height: 150px; + border-radius: 4px; + object-fit: cover; +} + +.play-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.youtube-preview { + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.youtube-preview .preview-image-container { + width: 100%; + max-width: 400px; + position: relative; +} + +.youtube-preview .preview-image { + width: 100%; + max-width: 100%; + max-height: none; + aspect-ratio: 16 / 9; +} + +.youtube-video-wrapper { + margin-top: 8px; + position: relative; + width: 100%; + max-width: 560px; + /* Standard YouTube embed width or max for chat */ + padding-bottom: 56.25%; + /* 16:9 Aspect Ratio */ + height: 0; + overflow: hidden; + border-radius: 4px; + background-color: black; +} + +.youtube-video-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; } \ No newline at end of file diff --git a/Frontend/Electron/src/pages/Login.jsx b/Frontend/Electron/src/pages/Login.jsx index c05efbe..eb0879f 100644 --- a/Frontend/Electron/src/pages/Login.jsx +++ b/Frontend/Electron/src/pages/Login.jsx @@ -42,11 +42,20 @@ const Login = () => { throw new Error(verifyData.error || 'Login failed'); } - console.log('Login verified'); + console.log('Login verified. Response data:', verifyData); + if (verifyData.userId) { + console.log('Saving userId to localStorage:', verifyData.userId); localStorage.setItem('userId', verifyData.userId); + } else { + console.error('MISSING USERID IN VERIFY RESPONSE!', verifyData); } + localStorage.setItem('username', username); + + // Verify immediate read back + console.log('Immediate localStorage read check:', localStorage.getItem('userId')); + navigate('/chat'); } catch (err) { console.error('Login error:', err); diff --git a/Frontend/Electron/src/pages/Register.jsx b/Frontend/Electron/src/pages/Register.jsx index 0a34e64..15b51b5 100644 --- a/Frontend/Electron/src/pages/Register.jsx +++ b/Frontend/Electron/src/pages/Register.jsx @@ -18,7 +18,7 @@ const Register = () => { // 1. Generate Salt and Master Key (MK) const salt = await window.cryptoAPI.randomBytes(16); - const mk = await window.cryptoAPI.randomBytes(16); // 128-bit MK + const mk = await window.cryptoAPI.randomBytes(32); // 256-bit MK for AES-256 console.log('Generated Salt and MK');