From 2571137bec4a45aa2585ea5b9e1ebd64dd57a600 Mon Sep 17 00:00:00 2001 From: 230406 <230406@epvc.pt> Date: Wed, 27 May 2026 11:18:16 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20login/registo=20funcional=20=E2=80=94=20?= =?UTF-8?q?signIn=20NextAuth,=20SessionProvider,=20NEXTAUTH=5FSECRET=20rea?= =?UTF-8?q?l,=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/auth/register/route.ts | 105 +++++++------ app/auth/login/page.tsx | 273 ++++++++++++++++++--------------- app/layout.tsx | 3 +- app/providers.tsx | 7 + 4 files changed, 215 insertions(+), 173 deletions(-) create mode 100644 app/providers.tsx diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 7145e59..4387d98 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -6,51 +6,66 @@ import { validateAge } from '@/lib/auth/age-validation'; import { sendEmail, buildWelcomeHtml } from '@/lib/email'; export async function POST(request: Request) { - const body = await request.json(); - const parsed = registerSchema.safeParse(body); + try { + const body = await request.json(); + const parsed = registerSchema.safeParse(body); - if (!parsed.success) { - const errors = parsed.error.flatten().fieldErrors; - const firstError = Object.values(errors).flat()[0]; - return NextResponse.json({ error: firstError ?? 'Dados inválidos.' }, { status: 400 }); + if (!parsed.success) { + const errors = parsed.error.flatten().fieldErrors; + const firstError = Object.values(errors).flat()[0]; + return NextResponse.json({ error: firstError ?? 'Dados inválidos.' }, { status: 400 }); + } + + const { name, email, password, birthdate, district } = parsed.data; + + // Validação de +18 sempre no servidor + const ageCheck = validateAge(birthdate); + if (!ageCheck.valid) { + return NextResponse.json({ error: ageCheck.error }, { status: 400 }); + } + + // Verificar se email já existe + const existing = await prisma.user.findUnique({ where: { email } }); + if (existing) { + return NextResponse.json({ error: 'Este email já está registado.' }, { status: 409 }); + } + + const hashedPassword = await hashPassword(password); + + const user = await prisma.user.create({ + data: { + name, + email, + password: hashedPassword, + birthdate: new Date(birthdate), + district, + }, + select: { id: true, name: true, email: true }, + }); + + // Email de boas-vindas (não bloqueia a resposta) + sendEmail({ + to: user.email, + subject: 'Bem-vindo à PetLink 🐾', + html: buildWelcomeHtml({ userName: user.name }), + }).catch(console.error); + + return NextResponse.json( + { message: 'Conta criada com sucesso.', userId: user.id }, + { status: 201 } + ); + } catch (err: unknown) { + console.error('[POST /api/auth/register]', err); + + // Erro de ligação à base de dados — mensagem clara ao utilizador + const msg = err instanceof Error ? err.message : ''; + if (msg.includes('P1001') || msg.includes('connect') || msg.includes('ECONNREFUSED')) { + return NextResponse.json( + { error: 'Base de dados temporariamente indisponível. Tenta mais tarde.' }, + { status: 503 } + ); + } + + return NextResponse.json({ error: 'Erro interno. Tenta de novo.' }, { status: 500 }); } - - const { name, email, password, birthdate, district } = parsed.data; - - // Validação de +18 sempre no servidor - const ageCheck = validateAge(birthdate); - if (!ageCheck.valid) { - return NextResponse.json({ error: ageCheck.error }, { status: 400 }); - } - - // Verificar se email já existe - const existing = await prisma.user.findUnique({ where: { email } }); - if (existing) { - return NextResponse.json({ error: 'Este email já está registado.' }, { status: 409 }); - } - - const hashedPassword = await hashPassword(password); - - const user = await prisma.user.create({ - data: { - name, - email, - password: hashedPassword, - birthdate: new Date(birthdate), - district, - }, - select: { id: true, name: true, email: true }, - }); - - // Email de boas-vindas (não bloqueia) - sendEmail({ - to: user.email, - subject: 'Bem-vindo à PetLink 🐾', - html: buildWelcomeHtml({ userName: user.name }), - }).catch(console.error); - - return NextResponse.json( - { message: 'Conta criada com sucesso.', userId: user.id }, - { status: 201 } - ); } diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 86c49e3..04e4c39 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -2,44 +2,89 @@ import { useState } from 'react'; import Link from 'next/link'; -import { PawPrint, Eye, EyeOff, Mail, Lock } from 'lucide-react'; -import type { Metadata } from 'next'; +import { useRouter } from 'next/navigation'; +import { signIn } from 'next-auth/react'; +import { PawPrint, Eye, EyeOff, Mail, Lock, AlertCircle } from 'lucide-react'; export default function LoginPage() { + const router = useRouter(); const [showPass, setShowPass] = useState(false); - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setError(''); setLoading(true); - // TODO: substituir por NextAuth signIn quando NEXTAUTH_SECRET estiver configurado - setTimeout(() => setLoading(false), 1200); + + try { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }); + + if (result?.error) { + setError('Email ou palavra-passe incorrectos. Tenta de novo.'); + } else { + router.push('/'); + router.refresh(); + } + } catch { + setError('Erro de rede. Verifica a tua ligação e tenta de novo.'); + } finally { + setLoading(false); + } + }; + + const inputStyle: React.CSSProperties = { + width: '100%', + padding: '12px 16px 12px 40px', + background: 'var(--color-bg, var(--cream))', + border: '1.5px solid var(--color-border, var(--parchment))', + borderRadius: '10px', + fontFamily: 'var(--font-body)', + fontSize: '15px', + color: 'var(--color-text, var(--soil))', + outline: 'none', + transition: 'border-color 180ms ease', + minHeight: '48px', + boxSizing: 'border-box', + }; + + const labelStyle: React.CSSProperties = { + fontFamily: 'var(--font-accent, monospace)', + fontSize: '11px', + fontWeight: 400, + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: 'var(--color-muted, var(--soil-faint))', }; return (
- {/* Brand mark */} + {/* Marca */}
- + PetLink @@ -48,39 +93,59 @@ export default function LoginPage() {

- Bem-vindo de volta. + Iniciar sessão.

-

- Não tens conta?{' '} - ((e.target as HTMLElement).style.textDecorationColor = 'var(--color-terra)')} - onMouseLeave={e => ((e.target as HTMLElement).style.textDecorationColor = 'transparent')} - > - Criar conta grátis +

+ Ainda não tens conta?{' '} + + Registar

+ {/* Erro */} + {error && ( +
+ + {error} +
+ )} +
- {/* Email field */} + {/* Email */}
- +
- + (e.target.style.borderColor = 'var(--color-terra)')} - onBlur={e => (e.target.style.borderColor = 'var(--color-border)')} + style={inputStyle} + onFocus={e => (e.target.style.borderColor = 'var(--color-terra, var(--terra))')} + onBlur={e => (e.target.style.borderColor = 'var(--color-border, var(--parchment))')} />
- {/* Password field */} + {/* Password */}
- + Esqueceste-a?
- + setPassword(e.target.value)} - placeholder="••••••••" + placeholder="A tua palavra-passe" required autoComplete="current-password" - style={{ - width: '100%', - padding: '12px 44px 12px 40px', - background: 'var(--color-bg)', - border: '1.5px solid var(--color-border)', - borderRadius: '10px', - fontFamily: 'var(--font-body)', - fontSize: '15px', - color: 'var(--color-text)', - outline: 'none', - transition: 'border-color 180ms ease', - minHeight: '48px', - }} - onFocus={e => (e.target.style.borderColor = 'var(--color-terra)')} - onBlur={e => (e.target.style.borderColor = 'var(--color-border)')} + style={{ ...inputStyle, paddingRight: '44px' }} + onFocus={e => (e.target.style.borderColor = 'var(--color-terra, var(--terra))')} + onBlur={e => (e.target.style.borderColor = 'var(--color-border, var(--parchment))')} /> + + {/* Registar */} +

+ Não tens conta?{' '} + + Criar conta grátis + +

- {/* Divider */} -
-
- ou -
-
- -

- Ao entrar, aceitas os nossos{' '} - Termos de Utilização - {' '}e a nossa{' '} - Política de Privacidade. -

-
); diff --git a/app/layout.tsx b/app/layout.tsx index a9570fc..05126d4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { Playfair_Display, Lora, Fragment_Mono } from 'next/font/google'; import Script from 'next/script'; +import Providers from './providers'; import './globals.css'; const playfair = Playfair_Display({ @@ -73,7 +74,7 @@ export default function RootLayout({ fontFamily: 'var(--font-lora, var(--font-body, Lora, Georgia, serif))', }} > - {children} + {children} ); diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 0000000..db649c3 --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { SessionProvider } from 'next-auth/react'; + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; +}