feat: Add initial Electron app structure with real-time chat, user authentication, and encrypted messaging.
This commit is contained in:
@@ -50,7 +50,7 @@ router.post('/login/verify', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.query(
|
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]
|
[username]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
162
Frontend/Electron/main.cjs
Normal file
162
Frontend/Electron/main.cjs
Normal file
@@ -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(`<meta\\s+(?:name|property)=["'](?:og:)?${prop}["']\\s+content=["'](.*?)["']`, 'i');
|
||||||
|
const match = data.match(regex);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
};
|
||||||
|
const getTitle = () => {
|
||||||
|
const regex = /<title>(.*?)<\/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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "main.js",
|
"main": "main.cjs",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('cryptoAPI', {
|
contextBridge.exposeInMainWorld('cryptoAPI', {
|
||||||
deriveAuthKeys: (password, salt) => ipcRenderer.invoke('crypto:deriveAuthKeys', password, salt),
|
generateKeys: () => ipcRenderer.invoke('generate-keys'),
|
||||||
encryptData: (plaintext, keyHex, ivHex) => ipcRenderer.invoke('crypto:encryptData', plaintext, keyHex, ivHex),
|
randomBytes: (size) => ipcRenderer.invoke('random-bytes', size),
|
||||||
decryptData: (ciphertext, keyHex, ivHex, tagHex) => ipcRenderer.invoke('crypto:decryptData', ciphertext, keyHex, ivHex, tagHex),
|
sha256: (data) => ipcRenderer.invoke('sha256', data),
|
||||||
generateKeys: () => ipcRenderer.invoke('crypto:generateKeys'),
|
deriveAuthKeys: (password, salt) => ipcRenderer.invoke('derive-auth-keys', password, salt),
|
||||||
randomBytes: (size) => ipcRenderer.invoke('crypto:randomBytes', size),
|
encryptData: (data, key) => ipcRenderer.invoke('encrypt-data', data, key),
|
||||||
sha256: (data) => ipcRenderer.invoke('crypto:sha256', data)
|
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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,20 +10,26 @@ const ChatArea = ({ channelId, username }) => {
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [socket, setSocket] = useState(null);
|
const [socket, setSocket] = useState(null);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
// Mock Key for demo (In real app, derive from Channel Key Bundle)
|
// Mock Key for demo (32 bytes hex = 64 chars)
|
||||||
const DEMO_CHANNEL_KEY = '000102030405060708090a0b0c0d0e0f';
|
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
|
// Helper to decrypt message
|
||||||
const decryptMessage = async (msg) => {
|
const decryptMessage = async (msg) => {
|
||||||
try {
|
try {
|
||||||
// Check if ciphertext has appended tag
|
|
||||||
// Tag is 16 bytes = 32 hex chars
|
|
||||||
const TAG_LENGTH = 32;
|
const TAG_LENGTH = 32;
|
||||||
if (!msg.ciphertext || msg.ciphertext.length < TAG_LENGTH) {
|
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]';
|
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(() => {
|
useEffect(() => {
|
||||||
const newSocket = io('http://localhost:3000');
|
const newSocket = io('http://localhost:3000');
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
@@ -64,32 +149,37 @@ const ChatArea = ({ channelId, username }) => {
|
|||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [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) => {
|
const handleSend = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!input.trim()) return;
|
if (!input.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Encrypt message
|
|
||||||
const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY);
|
const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY);
|
||||||
|
|
||||||
// Append tag to ciphertext for storage
|
|
||||||
const ciphertext = encryptedContent + tag;
|
const ciphertext = encryptedContent + tag;
|
||||||
|
|
||||||
// Sign message (placeholder)
|
|
||||||
const signature = 'placeholder_signature';
|
const signature = 'placeholder_signature';
|
||||||
|
|
||||||
|
const senderId = localStorage.getItem('userId');
|
||||||
|
if (!senderId) {
|
||||||
|
console.error('No userId found in localStorage');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const messageData = {
|
const messageData = {
|
||||||
channelId,
|
channelId,
|
||||||
senderId: '8b105be1-981e-4200-bb07-68d0714870c2', // Placeholder default, gets overwritten below
|
senderId,
|
||||||
ciphertext,
|
ciphertext,
|
||||||
nonce: iv,
|
nonce: iv,
|
||||||
signature,
|
signature,
|
||||||
keyVersion: 1
|
keyVersion: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const storedUserId = localStorage.getItem('userId');
|
|
||||||
if (storedUserId) messageData.senderId = storedUserId;
|
|
||||||
|
|
||||||
socket.emit('send_message', messageData);
|
socket.emit('send_message', messageData);
|
||||||
setInput('');
|
setInput('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -107,16 +197,43 @@ const ChatArea = ({ channelId, username }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="chat-area">
|
<div className="chat-area">
|
||||||
<div className="messages-list">
|
<div className="messages-list">
|
||||||
{messages.map((msg, idx) => (
|
{messages.map((msg, idx) => {
|
||||||
|
const urls = extractUrls(msg.content);
|
||||||
|
return (
|
||||||
<div key={idx} className="message-item">
|
<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">
|
<div className="message-header">
|
||||||
<span className="username">{msg.username || 'Unknown'}</span>
|
<span className="username" style={{ color: getUserColor(msg.username || 'Unknown') }}>
|
||||||
<span className="timestamp">{new Date(msg.created_at).toLocaleTimeString()}</span>
|
{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>
|
||||||
<div className="message-content">
|
<div className="message-content">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
pluginPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
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 }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
return !inline && match ? (
|
return !inline && match ? (
|
||||||
@@ -133,25 +250,61 @@ const ChatArea = ({ channelId, username }) => {
|
|||||||
{children}
|
{children}
|
||||||
</code>
|
</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}
|
{msg.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
{urls.map((url, i) => (
|
||||||
</div>
|
<LinkPreview key={i} url={url} />
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
<form className="chat-input-form" onSubmit={handleSend}>
|
<form className="chat-input-form" onSubmit={handleSend}>
|
||||||
|
<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
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={`Message #${channelId}`}
|
placeholder={`Message #${channelId}`}
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{ resize: 'none' }} // Disable manual resize
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-image: url('https://discord.com/assets/f9e794909795f472.svg');
|
background-image: url('https://discord.com/assets/f9e794909795f472.svg');
|
||||||
/* Placeholder background */
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
@@ -122,6 +121,7 @@ body {
|
|||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
min-width: 300px;
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -201,28 +201,71 @@ body {
|
|||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-list {
|
.messages-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
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 {
|
.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 {
|
.message-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
color: var(--header-primary);
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 8px;
|
margin-right: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,54 +275,267 @@ body {
|
|||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.375rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-form {
|
/* Markdown Styles Tweaks */
|
||||||
padding: 0 16px 24px;
|
.message-content strong {
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-form textarea {
|
.message-content h1,
|
||||||
width: 100%;
|
.message-content h2,
|
||||||
padding: 11px 16px;
|
.message-content h3 {
|
||||||
background-color: #40444b;
|
border-bottom: none;
|
||||||
border: none;
|
font-weight: 700;
|
||||||
border-radius: 8px;
|
color: var(--header-primary);
|
||||||
color: var(--text-normal);
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: inherit;
|
|
||||||
height: auto;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-form textarea:focus {
|
.message-content ul,
|
||||||
outline: none;
|
.message-content ol {
|
||||||
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown Styles */
|
.message-content a {
|
||||||
.message-content p {
|
color: #00b0f4;
|
||||||
margin: 0;
|
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 {
|
.message-content code {
|
||||||
background-color: #2f3136;
|
background-color: #2f3136;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content pre {
|
.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 {
|
.chat-input-form {
|
||||||
border-left: 4px solid var(--interactive-normal);
|
padding: 0 16px 24px;
|
||||||
margin: 0;
|
margin-top: 8px;
|
||||||
padding-left: 10px;
|
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;
|
||||||
}
|
}
|
||||||
@@ -42,11 +42,20 @@ const Login = () => {
|
|||||||
throw new Error(verifyData.error || 'Login failed');
|
throw new Error(verifyData.error || 'Login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Login verified');
|
console.log('Login verified. Response data:', verifyData);
|
||||||
|
|
||||||
if (verifyData.userId) {
|
if (verifyData.userId) {
|
||||||
|
console.log('Saving userId to localStorage:', verifyData.userId);
|
||||||
localStorage.setItem('userId', verifyData.userId);
|
localStorage.setItem('userId', verifyData.userId);
|
||||||
|
} else {
|
||||||
|
console.error('MISSING USERID IN VERIFY RESPONSE!', verifyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('username', username);
|
localStorage.setItem('username', username);
|
||||||
|
|
||||||
|
// Verify immediate read back
|
||||||
|
console.log('Immediate localStorage read check:', localStorage.getItem('userId'));
|
||||||
|
|
||||||
navigate('/chat');
|
navigate('/chat');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login error:', err);
|
console.error('Login error:', err);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const Register = () => {
|
|||||||
|
|
||||||
// 1. Generate Salt and Master Key (MK)
|
// 1. Generate Salt and Master Key (MK)
|
||||||
const salt = await window.cryptoAPI.randomBytes(16);
|
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');
|
console.log('Generated Salt and MK');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user