feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.
This commit is contained in:
@@ -3,12 +3,19 @@ import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useConvex } from 'convex/react';
|
||||
import { api } from '../../../../convex/_generated/api';
|
||||
|
||||
function parseInviteParams(input) {
|
||||
const codeMatch = input.match(/[?&]code=([^&]+)/);
|
||||
const keyMatch = input.match(/[?&]key=([^&]+)/);
|
||||
if (codeMatch && keyMatch) return { code: codeMatch[1], secret: keyMatch[1] };
|
||||
return null;
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inviteKeys, setInviteKeys] = useState(null); // { channelId: keyHex }
|
||||
const [inviteKeys, setInviteKeys] = useState(null);
|
||||
const [inviteLinkInput, setInviteLinkInput] = useState('');
|
||||
const [activeInviteCode, setActiveInviteCode] = useState(null);
|
||||
|
||||
@@ -16,7 +23,6 @@ const Register = () => {
|
||||
const location = useLocation();
|
||||
const convex = useConvex();
|
||||
|
||||
// Helper to process code/key
|
||||
const processInvite = async (code, secret) => {
|
||||
if (!window.cryptoAPI) {
|
||||
setError("Critical Error: Secure Crypto API missing. Run in Electron.");
|
||||
@@ -24,16 +30,13 @@ const Register = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 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 blob = JSON.parse(result.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);
|
||||
@@ -44,7 +47,6 @@ const Register = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Invite Link parsing from URL
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const code = params.get('code');
|
||||
@@ -57,17 +59,11 @@ const Register = () => {
|
||||
}, [location]);
|
||||
|
||||
const handleManualInvite = () => {
|
||||
try {
|
||||
const codeMatch = inviteLinkInput.match(/[?&]code=([^&]+)/);
|
||||
const keyMatch = inviteLinkInput.match(/[?&]key=([^&]+)/);
|
||||
|
||||
if (codeMatch && keyMatch) {
|
||||
processInvite(codeMatch[1], keyMatch[1]);
|
||||
} else {
|
||||
setError("Invalid invite link format.");
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Invalid URL.");
|
||||
const parsed = parseInviteParams(inviteLinkInput);
|
||||
if (parsed) {
|
||||
processInvite(parsed.code, parsed.secret);
|
||||
} else {
|
||||
setError("Invalid invite link format.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,36 +75,18 @@ const Register = () => {
|
||||
try {
|
||||
console.log('Starting registration for:', username);
|
||||
|
||||
// 1. Generate Salt and Master Key (MK)
|
||||
const salt = await window.cryptoAPI.randomBytes(16);
|
||||
const mk = await window.cryptoAPI.randomBytes(32);
|
||||
|
||||
console.log('Generated Salt and MK');
|
||||
|
||||
// 2. Derive Keys (DEK, DAK)
|
||||
const { dek, dak } = await window.cryptoAPI.deriveAuthKeys(password, salt);
|
||||
console.log('Derived keys');
|
||||
|
||||
// 3. Encrypt MK with DEK
|
||||
const encryptedMKObj = await window.cryptoAPI.encryptData(mk, dek);
|
||||
const encryptedMK = JSON.stringify(encryptedMKObj);
|
||||
|
||||
// 4. Hash DAK for Auth Proof
|
||||
const encryptedMK = JSON.stringify(await window.cryptoAPI.encryptData(mk, dek));
|
||||
const hak = await window.cryptoAPI.sha256(dak);
|
||||
|
||||
// 5. Generate Key Pairs
|
||||
const keys = await window.cryptoAPI.generateKeys();
|
||||
|
||||
// 6. Encrypt Private Keys with MK
|
||||
const encryptedRsaPriv = await window.cryptoAPI.encryptData(keys.rsaPriv, mk);
|
||||
const encryptedEdPriv = await window.cryptoAPI.encryptData(keys.edPriv, mk);
|
||||
|
||||
const encryptedPrivateKeys = JSON.stringify({
|
||||
rsa: encryptedRsaPriv,
|
||||
ed: encryptedEdPriv
|
||||
rsa: await window.cryptoAPI.encryptData(keys.rsaPriv, mk),
|
||||
ed: await window.cryptoAPI.encryptData(keys.edPriv, mk)
|
||||
});
|
||||
|
||||
// 7. Register via Convex
|
||||
const data = await convex.mutation(api.auth.createUserWithProfile, {
|
||||
username,
|
||||
salt,
|
||||
@@ -120,34 +98,25 @@ const Register = () => {
|
||||
inviteCode: activeInviteCode || undefined
|
||||
});
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.error) throw new Error(data.error);
|
||||
console.log('Registration successful:', data);
|
||||
|
||||
// 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)) {
|
||||
try {
|
||||
const batchKeys = await Promise.all(
|
||||
Object.entries(inviteKeys).map(async ([channelId, channelKeyHex]) => {
|
||||
const payload = JSON.stringify({ [channelId]: channelKeyHex });
|
||||
const encryptedKeyBundle = await window.cryptoAPI.publicEncrypt(keys.rsaPub, payload);
|
||||
return { channelId, userId: data.userId, encryptedKeyBundle, keyVersion: 1 };
|
||||
}).map(p => p.catch(err => {
|
||||
console.error('Failed to encrypt key for channel:', err);
|
||||
return null;
|
||||
}))
|
||||
);
|
||||
|
||||
batchKeys.push({
|
||||
channelId,
|
||||
userId: data.userId,
|
||||
encryptedKeyBundle,
|
||||
keyVersion: 1
|
||||
});
|
||||
} catch (keyErr) {
|
||||
console.error('Failed to encrypt key for channel:', channelId, keyErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (batchKeys.length > 0) {
|
||||
await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys });
|
||||
const validKeys = batchKeys.filter(Boolean);
|
||||
if (validKeys.length > 0) {
|
||||
await convex.mutation(api.channelKeys.uploadKeys, { keys: validKeys });
|
||||
console.log('Uploaded invite keys');
|
||||
}
|
||||
}
|
||||
@@ -170,24 +139,19 @@ const Register = () => {
|
||||
</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..."
|
||||
value={inviteLinkInput}
|
||||
onChange={(e) => setInviteLinkInput(e.target.value)}
|
||||
style={{ flex: 1, marginRight: '8px' }}
|
||||
/>
|
||||
<button type="button" onClick={handleManualInvite} className="auth-button" style={{ width: 'auto' }}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '15px', display: 'flex' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste Invite Link Here..."
|
||||
value={inviteLinkInput}
|
||||
onChange={(e) => setInviteLinkInput(e.target.value)}
|
||||
style={{ flex: 1, marginRight: '8px' }}
|
||||
/>
|
||||
<button type="button" onClick={handleManualInvite} className="auth-button" style={{ width: 'auto' }}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
)}
|
||||
|
||||
{inviteKeys ? (
|
||||
|
||||
Reference in New Issue
Block a user