feat: Implement core Discord clone functionality including Convex backend, Electron frontend, and UI components for chat, voice, and settings.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useConvex } from 'convex/react';
|
||||
import { api } from '../../../../convex/_generated/api';
|
||||
|
||||
const Register = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -12,6 +14,7 @@ const Register = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const convex = useConvex();
|
||||
|
||||
// Helper to process code/key
|
||||
const processInvite = async (code, secret) => {
|
||||
@@ -21,32 +24,32 @@ const Register = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch Invite
|
||||
const res = await fetch(`http://localhost:3000/api/invites/${code}`);
|
||||
if (!res.ok) throw new Error('Invalid or expired invite');
|
||||
const { encryptedPayload } = await res.json();
|
||||
|
||||
// Fetch Invite via Convex
|
||||
const result = await convex.query(api.invites.use, { code });
|
||||
if (result.error) throw new Error(result.error);
|
||||
const { encryptedPayload } = result;
|
||||
|
||||
// Decrypt Payload
|
||||
const blob = JSON.parse(encryptedPayload);
|
||||
const decrypted = await window.cryptoAPI.decryptData(blob.c, secret, blob.iv, blob.t);
|
||||
|
||||
|
||||
const keys = JSON.parse(decrypted);
|
||||
console.log('Invite keys decrypted successfully:', Object.keys(keys).length);
|
||||
setInviteKeys(keys);
|
||||
setActiveInviteCode(code); // Store code for backend validation
|
||||
setError(''); // Clear errors
|
||||
setActiveInviteCode(code);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error('Invite error:', err);
|
||||
setError('Invite verification failed: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Invite Link parsing from URL (if somehow navigated)
|
||||
// Handle Invite Link parsing from URL
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const code = params.get('code');
|
||||
const secret = params.get('key');
|
||||
|
||||
const secret = params.get('key');
|
||||
|
||||
if (code && secret) {
|
||||
console.log('Invite detected in URL');
|
||||
processInvite(code, secret);
|
||||
@@ -55,14 +58,6 @@ const Register = () => {
|
||||
|
||||
const handleManualInvite = () => {
|
||||
try {
|
||||
// Support full URL or just code? Full URL is easier for user (copy-paste)
|
||||
// Format: .../#/register?code=UUID&key=HEX
|
||||
const urlObj = new URL(inviteLinkInput);
|
||||
// In HashRouter, params are after #.
|
||||
// URL: http://.../#/register?code=X&key=Y
|
||||
// urlObj.hash -> "#/register?code=X&key=Y"
|
||||
|
||||
// We can just regex it to be safe
|
||||
const codeMatch = inviteLinkInput.match(/[?&]code=([^&]+)/);
|
||||
const keyMatch = inviteLinkInput.match(/[?&]key=([^&]+)/);
|
||||
|
||||
@@ -86,7 +81,7 @@ const Register = () => {
|
||||
|
||||
// 1. Generate Salt and Master Key (MK)
|
||||
const salt = await window.cryptoAPI.randomBytes(16);
|
||||
const mk = await window.cryptoAPI.randomBytes(32); // 256-bit MK for AES-256
|
||||
const mk = await window.cryptoAPI.randomBytes(32);
|
||||
|
||||
console.log('Generated Salt and MK');
|
||||
|
||||
@@ -96,7 +91,7 @@ const Register = () => {
|
||||
|
||||
// 3. Encrypt MK with DEK
|
||||
const encryptedMKObj = await window.cryptoAPI.encryptData(mk, dek);
|
||||
const encryptedMK = JSON.stringify(encryptedMKObj); // Store as JSON string {content, tag, iv}
|
||||
const encryptedMK = JSON.stringify(encryptedMKObj);
|
||||
|
||||
// 4. Hash DAK for Auth Proof
|
||||
const hak = await window.cryptoAPI.sha256(dak);
|
||||
@@ -113,8 +108,8 @@ const Register = () => {
|
||||
ed: encryptedEdPriv
|
||||
});
|
||||
|
||||
// 7. Send to Backend
|
||||
const payload = {
|
||||
// 7. Register via Convex
|
||||
const data = await convex.mutation(api.auth.createUserWithProfile, {
|
||||
username,
|
||||
salt,
|
||||
encryptedMK,
|
||||
@@ -122,19 +117,11 @@ const Register = () => {
|
||||
publicKey: keys.rsaPub,
|
||||
signingKey: keys.edPub,
|
||||
encryptedPrivateKeys,
|
||||
inviteCode: activeInviteCode // Enforce Invite
|
||||
};
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
inviteCode: activeInviteCode || undefined
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Registration failed');
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
console.log('Registration successful:', data);
|
||||
@@ -142,31 +129,27 @@ const Register = () => {
|
||||
// 8. Upload Invite Keys (If present)
|
||||
if (inviteKeys && data.userId) {
|
||||
console.log('Uploading invite keys...');
|
||||
const batchKeys = [];
|
||||
for (const [channelId, channelKeyHex] of Object.entries(inviteKeys)) {
|
||||
// Encrypt Channel Key with User's RSA Public Key
|
||||
// Hybrid Encrypt? No, for now simplistic: encrypt the 32-byte hex key string (64 chars) with RSA-2048.
|
||||
// RSA-2048 can encrypt ~200 bytes. 64 chars is fine.
|
||||
try {
|
||||
// Match Sidebar.jsx format: payload is JSON string { [channelId]: key }
|
||||
const payload = JSON.stringify({ [channelId]: channelKeyHex });
|
||||
const encryptedKeyBundle = await window.cryptoAPI.publicEncrypt(keys.rsaPub, payload);
|
||||
|
||||
// Upload
|
||||
await fetch('http://localhost:3000/api/channels/keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
channelId,
|
||||
userId: data.userId,
|
||||
encryptedKeyBundle,
|
||||
keyVersion: 1
|
||||
})
|
||||
|
||||
batchKeys.push({
|
||||
channelId,
|
||||
userId: data.userId,
|
||||
encryptedKeyBundle,
|
||||
keyVersion: 1
|
||||
});
|
||||
console.log(`Uploaded key for channel ${channelId}`);
|
||||
} catch (keyErr) {
|
||||
console.error('Failed to upload key for channel:', channelId, keyErr);
|
||||
console.error('Failed to encrypt key for channel:', channelId, keyErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (batchKeys.length > 0) {
|
||||
await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys });
|
||||
console.log('Uploaded invite keys');
|
||||
}
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
@@ -186,14 +169,14 @@ const Register = () => {
|
||||
<p>Join the secure chat! {inviteKeys ? '(Invite Active)' : ''}</p>
|
||||
</div>
|
||||
{error && <div style={{ color: 'red', marginBottom: 10, textAlign: 'center' }}>{error}</div>}
|
||||
|
||||
|
||||
{/* Manual Invite Input - Fallback for Desktop App */}
|
||||
{!inviteKeys && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste Invite Link Here..."
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste Invite Link Here..."
|
||||
value={inviteLinkInput}
|
||||
onChange={(e) => setInviteLinkInput(e.target.value)}
|
||||
style={{ flex: 1, marginRight: '8px' }}
|
||||
@@ -237,14 +220,14 @@ const Register = () => {
|
||||
<div style={{ textAlign: 'center', marginTop: '20px', color: '#b9bbbe' }}>
|
||||
<p>Registration is Invite-Only.</p>
|
||||
<p style={{ fontSize: '0.9em' }}>Please paste a valid invite link above to proceed.</p>
|
||||
|
||||
|
||||
{/* Backdoor for First User */}
|
||||
<p style={{ marginTop: '20px', fontSize: '0.8em', cursor: 'pointer', color: '#7289da' }} onClick={() => setInviteKeys({})}>
|
||||
(First User / Verify Setup)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="auth-footer">
|
||||
Already have an account? <Link to="/">Log In</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user