Implements comprehensive privacy and anonymity features including Tor hidden service support, location obfuscation, and anonymous registration. Privacy Features: - Anonymous plant registration with zero personal information - Location privacy levels: exact, fuzzy, city, country, hidden - Pseudonymous identities and wallet addresses - Privacy settings component with real-time Tor status - Encrypted anonymous contact generation Tor Integration: - SOCKS proxy support for Tor connections - Hidden service (.onion) configuration - Tor connection detection and status API - Docker Compose setup for easy Tor deployment - Automatic privacy warnings when not using Tor Location Obfuscation: - Fuzzy location: ±1-5km random offset - City level: ~10km grid - Country level: ~100km grid - Hidden: complete location privacy - Haversine-based distance calculations preserved Anonymous Registration: - /plants/register-anonymous endpoint - Privacy-first UI with Tor status banner - Anonymous IDs and wallet addresses - Optional pseudonym support - Encryption key support for enhanced security Infrastructure: - Tor service integration (lib/services/tor.ts) - Privacy utilities (lib/privacy/anonymity.ts) - PrivacySettings React component - Tor status API endpoint - Docker and docker-compose configurations - Example Tor configuration (torrc.example) Documentation: - Comprehensive TOR_SETUP.md guide - Installation instructions for Linux/macOS/Windows - Privacy best practices - Troubleshooting guide - Security considerations - Updated README with Tor features Dependencies: - Added socks-proxy-agent for Tor proxy support This enables: - Privacy-conscious growers to share anonymously - Protection of exact home locations - Censorship-resistant plant sharing - Community building without identity disclosure - Compliance with privacy regulations All privacy features are optional and configurable. Users can choose their desired privacy level.
227 lines
8 KiB
TypeScript
227 lines
8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { PrivacySettings as IPrivacySettings } from '../lib/privacy/anonymity';
|
||
|
||
interface PrivacySettingsProps {
|
||
value: IPrivacySettings;
|
||
onChange: (settings: IPrivacySettings) => void;
|
||
showTorStatus?: boolean;
|
||
}
|
||
|
||
export default function PrivacySettings({
|
||
value,
|
||
onChange,
|
||
showTorStatus = true,
|
||
}: PrivacySettingsProps) {
|
||
const [torStatus, setTorStatus] = useState<any>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
if (showTorStatus) {
|
||
checkTorStatus();
|
||
}
|
||
}, [showTorStatus]);
|
||
|
||
const checkTorStatus = async () => {
|
||
try {
|
||
const response = await fetch('/api/privacy/tor-status');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
setTorStatus(data);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking Tor status:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleChange = (field: keyof IPrivacySettings, newValue: any) => {
|
||
onChange({
|
||
...value,
|
||
[field]: newValue,
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="bg-white rounded-lg shadow-lg p-6 border-2 border-purple-200">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-xl font-bold text-gray-900 flex items-center">
|
||
🔒 Privacy & Anonymity Settings
|
||
</h2>
|
||
{!loading && torStatus?.tor.connectionThroughTor && (
|
||
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-semibold">
|
||
🧅 Tor Active
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tor Status Banner */}
|
||
{showTorStatus && !loading && (
|
||
<div
|
||
className={`mb-6 p-4 rounded-lg ${
|
||
torStatus?.tor.connectionThroughTor
|
||
? 'bg-green-50 border border-green-200'
|
||
: 'bg-yellow-50 border border-yellow-200'
|
||
}`}
|
||
>
|
||
<div className="flex items-start">
|
||
<span className="text-2xl mr-3">
|
||
{torStatus?.tor.connectionThroughTor ? '🧅' : '⚠️'}
|
||
</span>
|
||
<div className="flex-1">
|
||
<h3 className="font-semibold text-gray-900 mb-1">
|
||
{torStatus?.tor.connectionThroughTor
|
||
? 'Tor Connection Active'
|
||
: 'Not Using Tor'}
|
||
</h3>
|
||
<p className="text-sm text-gray-700 mb-2">
|
||
{torStatus?.tor.connectionThroughTor
|
||
? 'Your connection is anonymous and routed through the Tor network.'
|
||
: 'For maximum privacy, consider accessing via Tor Browser.'}
|
||
</p>
|
||
{torStatus?.tor.onionAddress && (
|
||
<p className="text-sm font-mono bg-gray-100 p-2 rounded mt-2">
|
||
Onion Address: {torStatus.tor.onionAddress}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Anonymous Mode Toggle */}
|
||
<div className="mb-6">
|
||
<label className="flex items-center cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={value.anonymousMode}
|
||
onChange={(e) => handleChange('anonymousMode', e.target.checked)}
|
||
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
|
||
/>
|
||
<span className="ml-3 text-gray-900 font-medium">
|
||
Enable Anonymous Mode
|
||
</span>
|
||
</label>
|
||
<p className="ml-8 text-sm text-gray-600 mt-1">
|
||
Generate random identifiers and hide personal information
|
||
</p>
|
||
</div>
|
||
|
||
{/* Location Privacy */}
|
||
<div className="mb-6">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Location Privacy Level
|
||
</label>
|
||
<select
|
||
value={value.locationPrivacy}
|
||
onChange={(e) =>
|
||
handleChange(
|
||
'locationPrivacy',
|
||
e.target.value as 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'
|
||
)
|
||
}
|
||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||
>
|
||
<option value="exact">📍 Exact Location (Public)</option>
|
||
<option value="fuzzy">🎯 Fuzzy (±1-5km radius)</option>
|
||
<option value="city">🏙️ City Level (~10km grid)</option>
|
||
<option value="country">🌍 Country/Region (~100km grid)</option>
|
||
<option value="hidden">🔒 Hidden (No location)</option>
|
||
</select>
|
||
<div className="mt-2 text-sm text-gray-600">
|
||
{value.locationPrivacy === 'exact' && (
|
||
<span className="text-red-600 font-medium">
|
||
⚠️ Warning: Exact location may reveal your home address
|
||
</span>
|
||
)}
|
||
{value.locationPrivacy === 'fuzzy' && (
|
||
<span>✓ Good balance of privacy and discoverability</span>
|
||
)}
|
||
{value.locationPrivacy === 'city' && (
|
||
<span>✓ Only city-level information shared</span>
|
||
)}
|
||
{value.locationPrivacy === 'country' && (
|
||
<span>✓ Only country/region visible</span>
|
||
)}
|
||
{value.locationPrivacy === 'hidden' && (
|
||
<span className="text-purple-600 font-medium">
|
||
🔒 Maximum privacy: Location completely hidden
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Identity Privacy */}
|
||
<div className="mb-6">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Identity Privacy
|
||
</label>
|
||
<select
|
||
value={value.identityPrivacy}
|
||
onChange={(e) =>
|
||
handleChange(
|
||
'identityPrivacy',
|
||
e.target.value as 'real' | 'pseudonym' | 'anonymous'
|
||
)
|
||
}
|
||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||
>
|
||
<option value="real">👤 Real Name</option>
|
||
<option value="pseudonym">🎭 Pseudonym</option>
|
||
<option value="anonymous">🔒 Anonymous</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Share Plant Details */}
|
||
<div className="mb-4">
|
||
<label className="flex items-center cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={value.sharePlantDetails}
|
||
onChange={(e) =>
|
||
handleChange('sharePlantDetails', e.target.checked)
|
||
}
|
||
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
|
||
/>
|
||
<span className="ml-3 text-gray-900 font-medium">
|
||
Share Plant Details (species, genus, family)
|
||
</span>
|
||
</label>
|
||
<p className="ml-8 text-sm text-gray-600 mt-1">
|
||
Uncheck to use generic plant identifiers
|
||
</p>
|
||
</div>
|
||
|
||
{/* Privacy Summary */}
|
||
<div className="mt-6 p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||
<h3 className="font-semibold text-purple-900 mb-2">Privacy Summary</h3>
|
||
<ul className="text-sm text-purple-800 space-y-1">
|
||
<li>
|
||
• Location: {value.locationPrivacy === 'exact' ? 'Visible to all' : 'Protected'}
|
||
</li>
|
||
<li>
|
||
• Identity: {value.identityPrivacy === 'real' ? 'Real name' : 'Protected'}
|
||
</li>
|
||
<li>
|
||
• Plant Info: {value.sharePlantDetails ? 'Shared' : 'Generic'}
|
||
</li>
|
||
<li>
|
||
• Mode: {value.anonymousMode ? 'Anonymous 🔒' : 'Standard'}
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{/* Recommendations */}
|
||
{!loading && torStatus && torStatus.recommendations && (
|
||
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||
<h3 className="font-semibold text-blue-900 mb-2">💡 Recommendations</h3>
|
||
<ul className="text-sm text-blue-800 space-y-1">
|
||
{torStatus.recommendations.map((rec: string, idx: number) => (
|
||
<li key={idx}>• {rec}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|