249 lines
15 KiB
TypeScript
249 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Shield, ArrowRight, Loader2, Key, ChevronRight, Lock } from 'lucide-react';
|
|
import api from '../lib/api';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { DevTools } from '../components/dev/DevTools';
|
|
import { pageVariants, itemVariants } from '../lib/animations';
|
|
|
|
export default function LoginPage() {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
document.title = 'Veridian | Authentication';
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const { data } = await api.post('/auth/login', { email, password });
|
|
login(data.accessToken, data.refreshToken, data.user);
|
|
navigate('/dashboard');
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Authentication failed. Please check your credentials.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[var(--color-bg-primary)] flex lg:overflow-hidden overflow-y-auto font-sans selection:bg-[var(--color-primary)]/30">
|
|
{/* Left Side: Brand/Visual */}
|
|
<div className="hidden lg:flex flex-1 relative items-center justify-center border-r border-[var(--color-border-subtle)] bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-[var(--color-bg-secondary)] via-[var(--color-bg-primary)] to-[var(--color-bg-primary)]">
|
|
<div className="absolute inset-0 opacity-10" style={{ backgroundImage: 'radial-gradient(var(--color-border-default) 1px, transparent 1px)', backgroundSize: '32px 32px' }} />
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 1, ease: "easeOut" }}
|
|
className="relative z-10 flex flex-col items-center text-center p-12 space-y-8"
|
|
>
|
|
<div className="relative group">
|
|
<img
|
|
src="/assets/logo-veridian.png"
|
|
alt="Veridian"
|
|
className="w-24 h-24 rounded-3xl shadow-2xl transition-transform duration-slow ease-out-expo group-hover:scale-105"
|
|
/>
|
|
<div className="absolute inset-0 rounded-3xl bg-[var(--color-primary)] opacity-0 group-hover:opacity-10 transition-opacity duration-normal blur-xl" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<h2 className="text-5xl font-bold tracking-tighter text-[var(--color-text-primary)]">
|
|
VERIDIAN
|
|
</h2>
|
|
<p className="text-[var(--color-text-tertiary)] font-mono text-sm tracking-[0.3em] uppercase">
|
|
Operations Infrastructure
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6 mt-12 bg-[var(--color-bg-tertiary)] backdrop-blur-md px-6 py-3 rounded-full border border-[var(--color-border-subtle)] shadow-xl">
|
|
<div className="flex -space-x-3">
|
|
{[1, 2, 3].map(i => (
|
|
<div key={i} className="w-8 h-8 rounded-full border-2 border-[var(--color-bg-primary)] bg-[var(--color-bg-secondary)]" />
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-[var(--color-text-tertiary)] font-medium">Operations Center</p>
|
|
<div className="w-2 h-2 rounded-full bg-[var(--color-primary)]" />
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Decorative elements */}
|
|
<div className="absolute bottom-12 left-12 text-[10px] font-mono text-[var(--color-text-quaternary)] tracking-tighter uppercase leading-none">
|
|
Veridian Operations<br />
|
|
Cultivation Platform<br />
|
|
v1.0.0
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side: Form */}
|
|
<div className="flex-1 flex flex-col items-center justify-center p-6 lg:p-24 relative">
|
|
<motion.div
|
|
variants={pageVariants}
|
|
initial="initial"
|
|
animate="animate"
|
|
className="w-full max-w-sm space-y-12"
|
|
>
|
|
<div className="space-y-3">
|
|
<motion.div
|
|
variants={itemVariants}
|
|
className="lg:hidden relative group w-fit mb-8 select-none active:scale-95 transition-transform"
|
|
onClick={async () => {
|
|
const now = Date.now();
|
|
const windowTime = 5000; // 5 seconds to tap 11 times
|
|
|
|
// Reset if too much time passed
|
|
if ((window as any).lastTap && now - (window as any).lastTap > 1000) {
|
|
(window as any).tapCount = 0;
|
|
}
|
|
|
|
(window as any).lastTap = now;
|
|
(window as any).tapCount = ((window as any).tapCount || 0) + 1;
|
|
|
|
if ((window as any).tapCount >= 11) {
|
|
(window as any).tapCount = 0;
|
|
setIsLoading(true);
|
|
try {
|
|
// Use native fetch instead of axios for better Capacitor WebView compatibility
|
|
const response = await fetch('https://veridian.runfoo.run/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: 'tenwest@proton.me',
|
|
password: '2GreenSlugs!'
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
login(data.accessToken, data.refreshToken, data.user);
|
|
navigate('/dashboard');
|
|
} catch (err: any) {
|
|
console.error('Auto-login error:', err);
|
|
setError(`Login failed: ${err.message}`);
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<img
|
|
src="/assets/logo-veridian.png"
|
|
alt="Veridian"
|
|
className="w-12 h-12 rounded-xl shadow-lg transition-transform duration-300 group-hover:scale-105 pointer-events-none"
|
|
/>
|
|
<div className="absolute inset-0 rounded-xl bg-[var(--color-primary)] opacity-0 group-hover:opacity-10 transition-opacity blur-lg" />
|
|
</motion.div>
|
|
<motion.h1 variants={itemVariants} className="text-3xl font-bold tracking-tight text-[var(--color-text-primary)] lg:text-4xl">
|
|
Sign In
|
|
</motion.h1>
|
|
<motion.p variants={itemVariants} className="text-[var(--color-text-tertiary)] text-sm">
|
|
Access your operations portal.
|
|
</motion.p>
|
|
</div>
|
|
|
|
<motion.form variants={itemVariants} onSubmit={handleSubmit} className="space-y-6">
|
|
<AnimatePresence mode="wait">
|
|
{error && (
|
|
<motion.div
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="p-4 bg-[var(--color-error)]/10 border border-rose-500/20 rounded-xl flex items-start gap-3"
|
|
>
|
|
<Shield className="text-[var(--color-error)] flex-shrink-0 mt-0.5" size={16} />
|
|
<p className="text-sm text-[var(--color-error)] leading-snug break-all font-mono text-xs">{error}</p>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-widest ml-1">Email</label>
|
|
<div className="relative group">
|
|
<input
|
|
type="email"
|
|
required
|
|
className="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-border-subtle)] rounded-xl px-4 py-3.5 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/40 focus:border-[var(--color-primary)] transition-all placeholder:text-[var(--color-text-quaternary)]"
|
|
placeholder="you@example.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
disabled={isLoading}
|
|
/>
|
|
<div className="absolute inset-0 rounded-xl bg-[var(--color-primary)]/5 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-bold text-[var(--color-text-tertiary)] uppercase tracking-widest ml-1">Password</label>
|
|
<div className="relative group">
|
|
<input
|
|
type="password"
|
|
required
|
|
className="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-border-subtle)] rounded-xl px-4 py-3.5 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/40 focus:border-[var(--color-primary)] transition-all placeholder:text-[var(--color-text-quaternary)]"
|
|
placeholder="••••••••••••"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
disabled={isLoading}
|
|
/>
|
|
<div className="absolute inset-0 rounded-xl bg-[var(--color-primary)]/5 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className="group relative w-full bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] rounded-xl h-14 font-bold tracking-tight shadow-xl transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-[100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
|
|
{isLoading ? (
|
|
<Loader2 className="animate-spin" size={20} />
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<span>Continue</span>
|
|
<ChevronRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
</motion.form>
|
|
|
|
<motion.div variants={itemVariants} className="pt-10 lg:pt-24 border-t border-[var(--color-border-subtle)] space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-[10px] font-bold text-[var(--color-text-quaternary)] uppercase tracking-widest">Global Security</p>
|
|
<div className="h-px flex-1 bg-[var(--color-border-subtle)] mx-4" />
|
|
<Shield className="text-[var(--color-text-quaternary)]" size={14} />
|
|
</div>
|
|
<p className="text-[11px] text-[var(--color-text-tertiary)] leading-relaxed text-center">
|
|
This system is for the exclusive use of authorized personnel. All activity is logged and monitored for compliance integrity.
|
|
</p>
|
|
<div className="flex justify-center pt-2">
|
|
<a
|
|
href="/veridian-v2.apk"
|
|
className="text-[10px] text-[var(--color-primary)] hover:text-[var(--color-primary-hover)] font-medium transition-colors flex items-center gap-1"
|
|
>
|
|
<Lock size={10} />
|
|
Download Android App (v2)
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<DevTools />
|
|
</div>
|
|
);
|
|
}
|