Files
PecHub/frontend/src/pages/Login/LoginPage.tsx
T
2026-03-18 20:54:43 +01:00

197 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useNavigate, Navigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { Mail, Lock, Eye, EyeOff, Shield } from 'lucide-react'
import { QRCodeSVG } from 'qrcode.react'
import toast from 'react-hot-toast'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import { useAuthStore } from '@/store/auth.store'
import { getErrorMessage } from '@/api/client'
interface LoginFormValues {
email: string
password: string
totp_code?: string
}
type LoginStep = 'credentials' | 'totp'
export function LoginPage() {
const navigate = useNavigate()
const { isAuthenticated, login, isLoading } = useAuthStore()
const [step, setStep] = useState<LoginStep>('credentials')
const [showPassword, setShowPassword] = useState(false)
const {
register,
handleSubmit,
getValues,
formState: { errors },
} = useForm<LoginFormValues>()
// Se già autenticato, vai all'inbox
if (isAuthenticated) {
return <Navigate to="/inbox" replace />
}
const onSubmit = async (data: LoginFormValues) => {
try {
await login({
email: data.email,
password: data.password,
totp_code: data.totp_code,
})
toast.success('Accesso effettuato con successo')
navigate('/inbox', { replace: true })
} catch (error) {
const msg = getErrorMessage(error)
// Se l'errore indica che serve il TOTP, passa al secondo step
if (msg.toLowerCase().includes('totp') || msg.toLowerCase().includes('otp') || msg.toLowerCase().includes('2fa')) {
setStep('totp')
return
}
toast.error(msg)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="w-full max-w-md space-y-6">
{/* Logo */}
<div className="text-center">
<div className="mx-auto h-16 w-16 rounded-2xl bg-blue-600 flex items-center justify-center shadow-lg">
<span className="text-white font-bold text-2xl">PF</span>
</div>
<h1 className="mt-4 text-3xl font-bold text-gray-900">PecFlow</h1>
<p className="mt-1 text-sm text-gray-500">Gestore PEC SaaS</p>
</div>
<Card className="shadow-sm">
{step === 'credentials' ? (
<>
<CardHeader>
<CardTitle className="text-xl">Accedi</CardTitle>
<CardDescription>Inserisci le tue credenziali per continuare</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Indirizzo email</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="email"
type="email"
placeholder="nome@azienda.it"
className="pl-10"
autoComplete="email"
{...register('email', {
required: 'Email obbligatoria',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Formato email non valido',
},
})}
/>
</div>
{errors.email && (
<p className="text-xs text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
className="pl-10 pr-10"
autoComplete="current-password"
{...register('password', { required: 'Password obbligatoria' })}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{errors.password && (
<p className="text-xs text-destructive">{errors.password.message}</p>
)}
</div>
<Button type="submit" className="w-full" isLoading={isLoading}>
Accedi
</Button>
</form>
</CardContent>
</>
) : (
<>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-600" />
<CardTitle className="text-xl">Verifica 2FA</CardTitle>
</div>
<CardDescription>
Inserisci il codice a 6 cifre dall'app autenticatore per{' '}
<strong>{getValues('email')}</strong>
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="totp_code">Codice OTP</Label>
<Input
id="totp_code"
type="text"
placeholder="000000"
maxLength={6}
className="text-center text-2xl tracking-widest font-mono"
autoComplete="one-time-code"
autoFocus
{...register('totp_code', {
required: 'Codice OTP obbligatorio',
minLength: { value: 6, message: 'Il codice deve avere 6 cifre' },
maxLength: { value: 6, message: 'Il codice deve avere 6 cifre' },
pattern: { value: /^\d{6}$/, message: 'Solo cifre (6 caratteri)' },
})}
/>
{errors.totp_code && (
<p className="text-xs text-destructive">{errors.totp_code.message}</p>
)}
</div>
<Button type="submit" className="w-full" isLoading={isLoading}>
Verifica
</Button>
<button
type="button"
onClick={() => setStep('credentials')}
className="w-full text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Torna alle credenziali
</button>
</form>
</CardContent>
</>
)}
</Card>
<p className="text-center text-xs text-gray-400">
PecFlow v1.0 Piattaforma gestione PEC certificata
</p>
</div>
</div>
)
}