localgreenchain/components/PrivacySettings.tsx
Claude ccea9535d4
Add Tor integration and privacy features for anonymous plant sharing
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.
2025-11-16 12:32:59 +00:00

227 lines
8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}