localgreenchain/pages/plants/register-anonymous.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

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