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.
360 lines
13 KiB
TypeScript
360 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { useRouter } from 'next/router';
|
|
import PrivacySettings from '../../components/PrivacySettings';
|
|
import { PrivacySettings as IPrivacySettings } from '../../lib/privacy/anonymity';
|
|
|
|
export default function RegisterAnonymousPlant() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
const [walletAddress, setWalletAddress] = useState('');
|
|
|
|
const [formData, setFormData] = useState({
|
|
commonName: '',
|
|
scientificName: '',
|
|
species: '',
|
|
genus: '',
|
|
family: '',
|
|
latitude: '',
|
|
longitude: '',
|
|
pseudonym: '',
|
|
encryptionKey: '',
|
|
});
|
|
|
|
const [privacySettings, setPrivacySettings] = useState<IPrivacySettings>({
|
|
anonymousMode: true,
|
|
locationPrivacy: 'fuzzy',
|
|
identityPrivacy: 'anonymous',
|
|
sharePlantDetails: true,
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const response = await fetch('/api/plants/register-anonymous', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
commonName: formData.commonName,
|
|
scientificName: formData.scientificName || undefined,
|
|
species: formData.species || undefined,
|
|
genus: formData.genus || undefined,
|
|
family: formData.family || undefined,
|
|
location: {
|
|
latitude: parseFloat(formData.latitude),
|
|
longitude: parseFloat(formData.longitude),
|
|
},
|
|
privacySettings,
|
|
pseudonym: formData.pseudonym || undefined,
|
|
encryptionKey: formData.encryptionKey || undefined,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to register plant');
|
|
}
|
|
|
|
setSuccess(true);
|
|
setWalletAddress(data.privacy.walletAddress);
|
|
|
|
setTimeout(() => {
|
|
router.push(`/plants/${data.plant.id}`);
|
|
}, 3000);
|
|
} catch (err: any) {
|
|
setError(err.message || 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
setFormData({
|
|
...formData,
|
|
[e.target.name]: e.target.value,
|
|
});
|
|
};
|
|
|
|
const getCurrentLocation = () => {
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
setFormData({
|
|
...formData,
|
|
latitude: position.coords.latitude.toString(),
|
|
longitude: position.coords.longitude.toString(),
|
|
});
|
|
},
|
|
(error) => {
|
|
setError('Unable to get your location: ' + error.message);
|
|
}
|
|
);
|
|
} else {
|
|
setError('Geolocation is not supported by your browser');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-100">
|
|
<Head>
|
|
<title>Anonymous Plant Registration - LocalGreenChain</title>
|
|
</Head>
|
|
|
|
{/* Header */}
|
|
<header className="bg-white shadow-sm border-b-2 border-purple-200">
|
|
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between">
|
|
<Link href="/">
|
|
<a className="text-2xl font-bold text-purple-800">
|
|
🌱 LocalGreenChain
|
|
</a>
|
|
</Link>
|
|
<nav className="flex gap-4">
|
|
<Link href="/plants/register">
|
|
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
|
|
Standard Registration
|
|
</a>
|
|
</Link>
|
|
<Link href="/plants/explore">
|
|
<a className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
|
|
Explore Network
|
|
</a>
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-6xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
|
<div className="mb-8 text-center">
|
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
|
🔒 Anonymous Plant Registration
|
|
</h1>
|
|
<p className="text-lg text-gray-600">
|
|
Register your plant with maximum privacy protection
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mb-6 p-6 bg-green-100 border border-green-400 text-green-700 rounded-lg">
|
|
<h3 className="font-bold text-lg mb-2">
|
|
✓ Plant registered anonymously!
|
|
</h3>
|
|
<p className="mb-2">Your anonymous wallet address:</p>
|
|
<p className="font-mono bg-white p-2 rounded text-sm break-all">
|
|
{walletAddress}
|
|
</p>
|
|
<p className="mt-2 text-sm">
|
|
Save this address to manage your plant. Redirecting...
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left Column - Privacy Settings */}
|
|
<div className="lg:col-span-1">
|
|
<PrivacySettings
|
|
value={privacySettings}
|
|
onChange={setPrivacySettings}
|
|
showTorStatus={true}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right Column - Plant Information */}
|
|
<div className="lg:col-span-2">
|
|
<div className="bg-white rounded-lg shadow-xl p-8">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
|
Plant Information
|
|
</h2>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Basic Plant Info */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Common Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="commonName"
|
|
required
|
|
value={formData.commonName}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
placeholder="e.g., Tomato, Basil"
|
|
/>
|
|
</div>
|
|
|
|
{privacySettings.sharePlantDetails && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Scientific Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="scientificName"
|
|
value={formData.scientificName}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
placeholder="e.g., Solanum lycopersicum"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Genus
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="genus"
|
|
value={formData.genus}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Family
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="family"
|
|
value={formData.family}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Location */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
Location (will be obfuscated based on privacy settings)
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Latitude *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
name="latitude"
|
|
required
|
|
value={formData.latitude}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Longitude *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
name="longitude"
|
|
required
|
|
value={formData.longitude}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<button
|
|
type="button"
|
|
onClick={getCurrentLocation}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
|
>
|
|
📍 Use My Current Location
|
|
</button>
|
|
<p className="mt-2 text-sm text-gray-600">
|
|
Your exact location will be obfuscated based on your
|
|
privacy settings
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Optional Pseudonym */}
|
|
{privacySettings.identityPrivacy === 'pseudonym' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Pseudonym (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="pseudonym"
|
|
value={formData.pseudonym}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
placeholder="Your chosen display name"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Encryption Key */}
|
|
{privacySettings.anonymousMode && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Encryption Key (optional)
|
|
</label>
|
|
<input
|
|
type="password"
|
|
name="encryptionKey"
|
|
value={formData.encryptionKey}
|
|
onChange={handleChange}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
placeholder="Optional password for extra security"
|
|
/>
|
|
<p className="mt-1 text-sm text-gray-600">
|
|
Used to generate your anonymous contact address
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit Button */}
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex-1 px-6 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? 'Registering...' : '🔒 Register Anonymously'}
|
|
</button>
|
|
<Link href="/">
|
|
<a className="px-6 py-3 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition">
|
|
Cancel
|
|
</a>
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|