feat: Add a large collection of emoji and other frontend assets, including a sound file, and a backend package.json.
This commit is contained in:
@@ -1,12 +1,80 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
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 [inviteLinkInput, setInviteLinkInput] = useState('');
|
||||
const [activeInviteCode, setActiveInviteCode] = useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Helper to process code/key
|
||||
const processInvite = async (code, secret) => {
|
||||
if (!window.cryptoAPI) {
|
||||
setError("Critical Error: Secure Crypto API missing. Run in Electron.");
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
} catch (err) {
|
||||
console.error('Invite error:', err);
|
||||
setError('Invite verification failed: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Invite Link parsing from URL (if somehow navigated)
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const code = params.get('code');
|
||||
const secret = params.get('key');
|
||||
|
||||
if (code && secret) {
|
||||
console.log('Invite detected in URL');
|
||||
processInvite(code, secret);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
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=([^&]+)/);
|
||||
|
||||
if (codeMatch && keyMatch) {
|
||||
processInvite(codeMatch[1], keyMatch[1]);
|
||||
} else {
|
||||
setError("Invalid invite link format.");
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Invalid URL.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -37,8 +105,6 @@ const Register = () => {
|
||||
const keys = await window.cryptoAPI.generateKeys();
|
||||
|
||||
// 6. Encrypt Private Keys with MK
|
||||
// We need to encrypt the private keys so the server can store them safely
|
||||
// MK is used to encrypt these.
|
||||
const encryptedRsaPriv = await window.cryptoAPI.encryptData(keys.rsaPriv, mk);
|
||||
const encryptedEdPriv = await window.cryptoAPI.encryptData(keys.edPriv, mk);
|
||||
|
||||
@@ -55,14 +121,10 @@ const Register = () => {
|
||||
hak,
|
||||
publicKey: keys.rsaPub,
|
||||
signingKey: keys.edPub,
|
||||
encryptedPrivateKeys // Note: Schema might need this column or we pack it into another
|
||||
encryptedPrivateKeys,
|
||||
inviteCode: activeInviteCode // Enforce Invite
|
||||
};
|
||||
|
||||
// NOTE: The schema in overview.md had 'Encrypted Private Keys' in the text but not explicitly in the SQL CREATE TABLE snippet provided in the prompt's overview.md (it was in the text description).
|
||||
// The SQL snippet had: encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key.
|
||||
// It did NOT have a column for encrypted_private_keys in the SQL block in overview.md.
|
||||
// I should check schema.sql I created.
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -76,6 +138,37 @@ const Register = () => {
|
||||
}
|
||||
|
||||
console.log('Registration successful:', data);
|
||||
|
||||
// 8. Upload Invite Keys (If present)
|
||||
if (inviteKeys && data.userId) {
|
||||
console.log('Uploading invite keys...');
|
||||
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
|
||||
})
|
||||
});
|
||||
console.log(`Uploaded key for channel ${channelId}`);
|
||||
} catch (keyErr) {
|
||||
console.error('Failed to upload key for channel:', channelId, keyErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
console.error('Registration error:', err);
|
||||
@@ -90,34 +183,68 @@ const Register = () => {
|
||||
<div className="auth-box">
|
||||
<div className="auth-header">
|
||||
<h2>Create an Account</h2>
|
||||
<p>Join the secure chat!</p>
|
||||
<p>Join the secure chat! {inviteKeys ? '(Invite Active)' : ''}</p>
|
||||
</div>
|
||||
{error && <div style={{ color: 'red', marginBottom: 10, textAlign: 'center' }}>{error}</div>}
|
||||
<form onSubmit={handleRegister}>
|
||||
<div className="form-group">
|
||||
<label>Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<div className="form-group">
|
||||
<label>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
|
||||
)}
|
||||
|
||||
{inviteKeys ? (
|
||||
<form onSubmit={handleRegister}>
|
||||
<div className="form-group">
|
||||
<label>Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="auth-button" disabled={loading}>
|
||||
{loading ? 'Generating Keys...' : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<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>
|
||||
<button type="submit" className="auth-button" disabled={loading}>
|
||||
{loading ? 'Generating Keys...' : 'Continue'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="auth-footer">
|
||||
Already have an account? <Link to="/">Log In</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user