2e688c8e52
The application source was untracked, so this commit brings it under version control together with fixes for the issues found while auditing it. Notable fixes: Authorization - Require a session and scope to senderEmail on /api/transfers/[id] (GET, DELETE) and .../resend. These were unauthenticated over an autoincrement id, so the ids could be walked to soft-delete any transfer, read sender/recipient metadata, or make the app mail arbitrary recipients. - Require a session on the legacy /api/send and /api/chunk-upload endpoints, and take the sender from the session rather than a request field so transfers cannot be posted as another user. Encryption - Replace the chunk encryption scheme. Every chunk was encrypted under one shared session IV with its auth tag discarded, which reuses the AES-GCM keystream (XORing two ciphertexts recovers plaintext without the key) and left the stored file undecryptable, surfacing to users as a wrong-password error. Chunks are now self-contained frames carrying their own random IV and auth tag, behind a magic+salt header. - Files written by the previous format now report UNSUPPORTED_FORMAT instead of a misleading password error. Download - Verify the password against the stored bcrypt hash before serving a file, and enforce expiresAt and DELETED/EXPIRED status. - Move the password from the query string into a POST body so it stays out of access logs and Referer headers. - Record a download only after successful authentication. - Decrypt frame by frame through a stream instead of buffering the whole file, and encode the Content-Disposition filename per RFC 5987. Data exposure - /api/download ran before the password prompt and returned the full transfer row, including absolute server file paths. It now returns only what the pre-password screen renders; filenames, message and recipient are withheld until /api/verify succeeds. Correctness - Fix BigInt handling that made /api/transfers and /api/transfers/[id] fail unconditionally (JSON.stringify cannot serialize BigInt, and seeding a BigInt reduce with 0 throws). - Fail loudly on a missing chunk during reassembly rather than silently writing a corrupt file. - Meter plan usage in plaintext bytes rather than on-disk encrypted size. Ignore /uploads: it holds runtime transfer payloads, not source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
310 lines
13 KiB
TypeScript
310 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Header } from '@/components/header';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Check,
|
|
X,
|
|
Zap,
|
|
Crown,
|
|
Gift,
|
|
Upload,
|
|
Clock,
|
|
Shield,
|
|
Users,
|
|
Mail,
|
|
Settings,
|
|
Infinity
|
|
} from 'lucide-react';
|
|
|
|
interface PricingTier {
|
|
name: string;
|
|
price: string;
|
|
period: string;
|
|
description: string;
|
|
icon: React.ComponentType<any>;
|
|
popular?: boolean;
|
|
features: {
|
|
name: string;
|
|
included: boolean;
|
|
value?: string;
|
|
}[];
|
|
cta: string;
|
|
color: string;
|
|
}
|
|
|
|
export default function PricingPage() {
|
|
const [isAnnual, setIsAnnual] = useState(false);
|
|
|
|
const tiers: PricingTier[] = [
|
|
{
|
|
name: 'Free',
|
|
price: '$0',
|
|
period: 'forever',
|
|
description: 'Perfect for personal use and trying out Transfer Tribe',
|
|
icon: Gift,
|
|
color: 'from-slate-500 to-slate-600',
|
|
cta: 'Get Started Free',
|
|
features: [
|
|
{ name: 'File size limit', included: true, value: '2GB per transfer' },
|
|
{ name: 'Monthly transfers', included: true, value: '10 transfers' },
|
|
{ name: 'Transfer expiration', included: true, value: '48 hours' },
|
|
{ name: 'Password protection', included: true },
|
|
{ name: 'Email notifications', included: true },
|
|
{ name: 'Basic support', included: true },
|
|
{ name: 'Custom branding', included: false },
|
|
{ name: 'Extended expiration', included: false },
|
|
{ name: 'Priority support', included: false },
|
|
{ name: 'Advanced analytics', included: false },
|
|
],
|
|
},
|
|
{
|
|
name: 'Rookie',
|
|
price: isAnnual ? '$8' : '$10',
|
|
period: isAnnual ? 'per month (billed annually)' : 'per month',
|
|
description: 'Ideal for professionals and small teams who need more flexibility',
|
|
icon: Zap,
|
|
popular: true,
|
|
color: 'from-blue-500 to-indigo-600',
|
|
cta: 'Start Free Trial',
|
|
features: [
|
|
{ name: 'File size limit', included: true, value: '10GB per transfer' },
|
|
{ name: 'Monthly transfers', included: true, value: '100 transfers' },
|
|
{ name: 'Transfer expiration', included: true, value: 'Up to 7 days' },
|
|
{ name: 'Password protection', included: true },
|
|
{ name: 'Email notifications', included: true },
|
|
{ name: 'Priority support', included: true },
|
|
{ name: 'Download tracking', included: true },
|
|
{ name: 'Transfer history', included: true, value: '6 months' },
|
|
{ name: 'Custom branding', included: false },
|
|
{ name: 'Advanced analytics', included: false },
|
|
],
|
|
},
|
|
{
|
|
name: 'Pro',
|
|
price: isAnnual ? '$20' : '$25',
|
|
period: isAnnual ? 'per month (billed annually)' : 'per month',
|
|
description: 'For businesses and power users who need maximum control',
|
|
icon: Crown,
|
|
color: 'from-purple-500 to-pink-600',
|
|
cta: 'Start Free Trial',
|
|
features: [
|
|
{ name: 'File size limit', included: true, value: 'Unlimited' },
|
|
{ name: 'Monthly transfers', included: true, value: 'Unlimited' },
|
|
{ name: 'Transfer expiration', included: true, value: 'Custom (1-30 days)' },
|
|
{ name: 'Password protection', included: true },
|
|
{ name: 'Email notifications', included: true },
|
|
{ name: 'Priority support', included: true },
|
|
{ name: 'Download tracking', included: true },
|
|
{ name: 'Transfer history', included: true, value: 'Unlimited' },
|
|
{ name: 'Custom branding', included: true },
|
|
{ name: 'Advanced analytics', included: true },
|
|
],
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
|
<Header />
|
|
|
|
<main className="container mx-auto px-4 py-12">
|
|
<div className="text-center mb-12">
|
|
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-slate-900 via-blue-600 to-indigo-600 dark:from-slate-100 dark:via-blue-400 dark:to-indigo-400 bg-clip-text text-transparent mb-4">
|
|
Choose Your Plan
|
|
</h1>
|
|
<p className="text-xl text-slate-600 dark:text-slate-400 max-w-2xl mx-auto mb-8">
|
|
Start free and upgrade as you grow. All plans include our core security features and 99.9% uptime guarantee.
|
|
</p>
|
|
|
|
<div className="flex items-center justify-center gap-4 mb-8">
|
|
<span className={`text-sm font-medium ${!isAnnual ? 'text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}`}>
|
|
Monthly
|
|
</span>
|
|
<button
|
|
onClick={() => setIsAnnual(!isAnnual)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
isAnnual ? 'bg-blue-600' : 'bg-slate-200 dark:bg-slate-700'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
isAnnual ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span className={`text-sm font-medium ${isAnnual ? 'text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}`}>
|
|
Annual
|
|
</span>
|
|
{isAnnual && (
|
|
<Badge className="bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
|
Save 20%
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
|
{tiers.map((tier, index) => {
|
|
const IconComponent = tier.icon;
|
|
return (
|
|
<Card
|
|
key={tier.name}
|
|
className={`relative bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border-white/20 dark:border-slate-700/20 hover:bg-white/90 dark:hover:bg-slate-800/90 transition-all ${
|
|
tier.popular ? 'ring-2 ring-blue-500 scale-105' : ''
|
|
}`}
|
|
>
|
|
{tier.popular && (
|
|
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
|
<Badge className="bg-gradient-to-r from-blue-500 to-indigo-600 text-white px-4 py-1">
|
|
Most Popular
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
<CardHeader className="text-center pb-8">
|
|
<div className={`w-16 h-16 bg-gradient-to-br ${tier.color} rounded-full flex items-center justify-center mx-auto mb-4`}>
|
|
<IconComponent className="w-8 h-8 text-white" />
|
|
</div>
|
|
<CardTitle className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
{tier.name}
|
|
</CardTitle>
|
|
<div className="mt-4">
|
|
<span className="text-4xl font-bold text-slate-900 dark:text-slate-100">
|
|
{tier.price}
|
|
</span>
|
|
<span className="text-slate-500 dark:text-slate-400 ml-2">
|
|
{tier.period}
|
|
</span>
|
|
</div>
|
|
<p className="text-slate-600 dark:text-slate-400 mt-4">
|
|
{tier.description}
|
|
</p>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-6">
|
|
<Button
|
|
className={`w-full ${
|
|
tier.popular
|
|
? 'bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700'
|
|
: 'bg-gradient-to-r from-slate-600 to-slate-700 hover:from-slate-700 hover:to-slate-800'
|
|
}`}
|
|
size="lg"
|
|
>
|
|
{tier.cta}
|
|
</Button>
|
|
|
|
<div className="space-y-4">
|
|
<h4 className="font-semibold text-slate-900 dark:text-slate-100">
|
|
What's included:
|
|
</h4>
|
|
<ul className="space-y-3">
|
|
{tier.features.map((feature, featureIndex) => (
|
|
<li key={featureIndex} className="flex items-start gap-3">
|
|
{feature.included ? (
|
|
<Check className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
|
|
) : (
|
|
<X className="w-5 h-5 text-slate-400 mt-0.5 flex-shrink-0" />
|
|
)}
|
|
<div className="flex-1">
|
|
<span className={`text-sm ${
|
|
feature.included
|
|
? 'text-slate-700 dark:text-slate-300'
|
|
: 'text-slate-400 dark:text-slate-500'
|
|
}`}>
|
|
{feature.name}
|
|
</span>
|
|
{feature.value && (
|
|
<span className={`block text-xs ${
|
|
feature.included
|
|
? 'text-slate-500 dark:text-slate-400'
|
|
: 'text-slate-400 dark:text-slate-500'
|
|
}`}>
|
|
{feature.value}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-16 text-center">
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-8">
|
|
Why Choose Transfer Tribe?
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 max-w-4xl mx-auto">
|
|
<div className="flex flex-col items-center space-y-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-emerald-600 rounded-full flex items-center justify-center">
|
|
<Shield className="w-6 h-6 text-white" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Enterprise Security</h3>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400 text-center">
|
|
Bank-level encryption and security protocols
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center space-y-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center">
|
|
<Upload className="w-6 h-6 text-white" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Lightning Fast</h3>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400 text-center">
|
|
Powered by Google Cloud for maximum speed
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center space-y-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-purple-400 to-purple-600 rounded-full flex items-center justify-center">
|
|
<Users className="w-6 h-6 text-white" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Team Friendly</h3>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400 text-center">
|
|
Perfect for individuals and teams of any size
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center space-y-3">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-orange-400 to-red-600 rounded-full flex items-center justify-center">
|
|
<Mail className="w-6 h-6 text-white" />
|
|
</div>
|
|
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Email Integration</h3>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400 text-center">
|
|
Seamless email delivery and notifications
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-16 bg-white/60 dark:bg-slate-800/60 backdrop-blur-sm rounded-2xl p-8 text-center border border-white/20 dark:border-slate-700/20">
|
|
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-4">
|
|
Need a Custom Solution?
|
|
</h2>
|
|
<p className="text-slate-600 dark:text-slate-400 mb-6 max-w-2xl mx-auto">
|
|
For enterprise customers with specific requirements, we offer custom plans with dedicated support,
|
|
advanced integrations, and tailored features.
|
|
</p>
|
|
<Button variant="outline" size="lg" className="mr-4">
|
|
Contact Sales
|
|
</Button>
|
|
<Button variant="outline" size="lg">
|
|
View Enterprise Features
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="mt-12 text-center">
|
|
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
All plans include a 14-day free trial. No credit card required. Cancel anytime.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|