From 3aa6e5468d11a8826a1b9c3c8cc55d530b05afa7 Mon Sep 17 00:00:00 2001 From: 230409 <230409@epvc.pt> Date: Tue, 26 May 2026 16:40:01 +0100 Subject: [PATCH] antes de alterar login --- .../app/(auth)/login/page.tsx | 46 ++++++++++- .../app/(auth)/register/page.tsx | 48 ++++++++++- .../app/(dashboard)/page.tsx | 9 ++ .../components/auth/AuthGuard.tsx | 5 +- .../dashboard/NotificationMonitor.tsx | 25 ++++-- .../contexts/AuthContext.tsx | 82 +++++++++++++++++-- reserva-mesa-dashboard/hooks/useMesas.ts | 44 +++++----- reserva-mesa-dashboard/hooks/useReservas.ts | 50 ++++++----- reserva-mesa-dashboard/hooks/useStaff.ts | 38 +++++---- reserva-mesa-dashboard/lib/firebase.ts | 53 +++++++++--- reserva-mesa-dashboard/package-lock.json | 2 +- reserva-mesa-dashboard/package.json | 6 +- 12 files changed, 316 insertions(+), 92 deletions(-) diff --git a/reserva-mesa-dashboard/app/(auth)/login/page.tsx b/reserva-mesa-dashboard/app/(auth)/login/page.tsx index 73c8870..9f94d84 100644 --- a/reserva-mesa-dashboard/app/(auth)/login/page.tsx +++ b/reserva-mesa-dashboard/app/(auth)/login/page.tsx @@ -23,11 +23,51 @@ export default function LoginPage() { setLoading(true); try { - await signInWithEmailAndPassword(auth, email, password); + const userCredential = await signInWithEmailAndPassword(auth, email, password); + + // Verify the user has a restaurant record before redirecting + if (!userCredential.user?.email) { + setError("Erro: Utilizador sem email válido."); + return; + } + + // Success — redirect to dashboard router.push("/"); } catch (err: any) { - console.error(err); - setError("Credenciais inválidas. Verifique o seu email e palavra-passe."); + console.error("[Login Error]", err.code, err.message); + + // Map Firebase Auth error codes to user-friendly messages in Portuguese + switch (err.code) { + case "auth/user-not-found": + setError("Esta conta não existe. Verifique o email ou registe-se."); + break; + case "auth/wrong-password": + setError("Palavra-passe incorrecta. Tente novamente."); + break; + case "auth/invalid-email": + setError("Email inválido. Verifique o formato do email."); + break; + case "auth/invalid-credential": + setError("Credenciais inválidas. Verifique o email e a palavra-passe."); + break; + case "auth/too-many-requests": + setError("Demasiadas tentativas falhadas. Aguarde alguns minutos e tente novamente."); + break; + case "auth/invalid-verification-id": + setError("Sessão expirada. Por favor, recarregue a página e tente novamente."); + break; + case "auth/network-request-failed": + setError("Erro de conexão. Verifique a sua ligação à internet."); + break; + case "auth/weak-password": + setError("A palavra-passe é demasiado curta. Deve ter pelo menos 6 caracteres."); + break; + case "auth/popup-closed-by-user": + // User closed the popup — no error needed + break; + default: + setError("Erro ao entrar. Verifique as suas credenciais e tente novamente."); + } } finally { setLoading(false); } diff --git a/reserva-mesa-dashboard/app/(auth)/register/page.tsx b/reserva-mesa-dashboard/app/(auth)/register/page.tsx index 402df26..73ae056 100644 --- a/reserva-mesa-dashboard/app/(auth)/register/page.tsx +++ b/reserva-mesa-dashboard/app/(auth)/register/page.tsx @@ -37,6 +37,23 @@ export default function RegisterPage() { setError(""); setLoading(true); + // Validate inputs before attempting registration + if (!formData.email || !formData.email.includes("@")) { + setError("Por favor, insira um email válido."); + setLoading(false); + return; + } + if (formData.password.length < 6) { + setError("A palavra-passe deve ter pelo menos 6 caracteres."); + setLoading(false); + return; + } + if (!formData.establishmentName.trim()) { + setError("Por favor, insira o nome do restaurante."); + setLoading(false); + return; + } + try { // 1. Criar utilizador na Firebase Auth const userCredential = await createUserWithEmailAndPassword(auth, formData.email, formData.password); @@ -62,10 +79,37 @@ export default function RegisterPage() { // 3. Gravar na Realtime Database em /Restaurantes await set(ref(db, `Restaurantes/${documentId}`), payload); + // 4. Success — redirect to dashboard router.push("/"); } catch (err: any) { - console.error(err); - setError(err.message || "Ocorreu um erro ao registar o restaurante."); + console.error("[Register Error]", err.code, err.message); + + // Map Firebase Auth error codes to user-friendly messages in Portuguese + switch (err.code) { + case "auth/email-already-in-use": + setError("Este email já está registado. Tente fazer login."); + break; + case "auth/weak-password": + setError("A palavra-passe deve ter pelo menos 6 caracteres."); + break; + case "auth/invalid-email": + setError("Email inválido. Verifique o formato do email."); + break; + case "auth/operation-not-allowed": + setError("Registo de contas está desactivado. Contacte o suporte."); + break; + case "auth/network-request-failed": + setError("Erro de conexão. Verifique a sua ligação à internet."); + break; + case "auth/too-many-requests": + setError("Demasiadas tentativas. Aguarde alguns minutos e tente novamente."); + break; + case "auth/invalid-credential": + setError("Credenciais inválidas."); + break; + default: + setError(`Erro ao criar conta: ${err.message || "Tente novamente."}`); + } } finally { setLoading(false); } diff --git a/reserva-mesa-dashboard/app/(dashboard)/page.tsx b/reserva-mesa-dashboard/app/(dashboard)/page.tsx index 39f48d4..635970c 100644 --- a/reserva-mesa-dashboard/app/(dashboard)/page.tsx +++ b/reserva-mesa-dashboard/app/(dashboard)/page.tsx @@ -21,6 +21,15 @@ export default function DashboardHomePage() { const { mesas, loading: loadingMesas } = useMesas(); const { staff } = useStaff(); + // Guard against missing user data + if (!user) { + return ( +
+
A carregar...
+
+ ); + } + // 1. Calculate top stats const todayStr = new Date().toISOString().split('T')[0]; const todayReservas = reservas.filter(r => r.data === todayStr || r.estado.startsWith("Confirmada")); diff --git a/reserva-mesa-dashboard/components/auth/AuthGuard.tsx b/reserva-mesa-dashboard/components/auth/AuthGuard.tsx index 1209e72..f6bd417 100644 --- a/reserva-mesa-dashboard/components/auth/AuthGuard.tsx +++ b/reserva-mesa-dashboard/components/auth/AuthGuard.tsx @@ -11,6 +11,7 @@ export default function AuthGuard({ children }: { children: React.ReactNode }) { useEffect(() => { if (!loading) { + // If user is not authenticated and not on a public page, redirect to login if (!user && !pathname.startsWith("/login") && !pathname.startsWith("/register")) { router.push("/login"); } @@ -25,8 +26,8 @@ export default function AuthGuard({ children }: { children: React.ReactNode }) { ); } - // Se não estiver logado e não estiver numa rota pública, não renderiza nada - // (o useEffect vai redirecionar) + // If not authenticated and not on a public page, don't render children + // (the useEffect will redirect) if (!user && !pathname.startsWith("/login") && !pathname.startsWith("/register")) { return null; } diff --git a/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx b/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx index da207e1..218378c 100644 --- a/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx +++ b/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx @@ -20,12 +20,17 @@ export function NotificationMonitor() { // Primeiro, marcamos todas as reservas existentes como "vistas" // para não disparar notificações para o passado const loadExisting = async () => { - const snapshot = await get(reservasRef); - if (snapshot.exists()) { - const data = snapshot.val(); - Object.keys(data).forEach(id => seenReservas.current.add(id)); + try { + const snapshot = await get(reservasRef); + if (snapshot.exists()) { + const data = snapshot.val(); + Object.keys(data).forEach(id => seenReservas.current.add(id)); + } + } catch (error) { + console.error("[NotificationMonitor] Error loading existing reservas:", error); + } finally { + isInitialLoad.current = false; } - isInitialLoad.current = false; }; loadExisting(); @@ -40,9 +45,13 @@ export function NotificationMonitor() { // Se ainda estivermos no load inicial (do get), ignoramos o toast if (isInitialLoad.current) return; - const data = snapshot.val(); - if (data.restauranteEmail === user.email && data.estado === "Pendente") { - toast(`Nova reserva recebida de ${data.clienteEmail}!`, "info"); + try { + const data = snapshot.val(); + if (data?.restauranteEmail === user.email && data?.estado === "Pendente") { + toast(`Nova reserva recebida de ${data?.clienteEmail || "cliente"}!`, "info"); + } + } catch (error) { + console.error("[NotificationMonitor] Error processing new reserva:", error); } }); diff --git a/reserva-mesa-dashboard/contexts/AuthContext.tsx b/reserva-mesa-dashboard/contexts/AuthContext.tsx index cdba70d..9416145 100644 --- a/reserva-mesa-dashboard/contexts/AuthContext.tsx +++ b/reserva-mesa-dashboard/contexts/AuthContext.tsx @@ -33,34 +33,106 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { return email.replace(/\./g, "_").replace(/@/g, "_at_"); }; + // Search for user by UID across all Restaurantes documents + const findUserByUid = async (uid: string, currentUser: FirebaseUser): Promise => { + try { + const restaurantsRef = ref(db, "Restaurantes"); + const snapshot = await get(restaurantsRef); + if (!snapshot.exists()) return null; + + const data = snapshot.val(); + for (const key of Object.keys(data)) { + const item = data[key]; + if (item?.uid === uid) { + return { ...item, email: item.email || currentUser.email } as RestaurantUser; + } + } + } catch (error) { + console.error("[Auth] Error searching by UID:", error); + } + return null; + }; + + // Search for user by email across all Restaurantes documents + const findUserByEmail = async (email: string): Promise => { + try { + const restaurantsRef = ref(db, "Restaurantes"); + const snapshot = await get(restaurantsRef); + if (!snapshot.exists()) return null; + + const data = snapshot.val(); + for (const key of Object.keys(data)) { + const item = data[key]; + if (item?.email === email || item?.establishmentEmail === email || item?.ownerEmail === email) { + return { ...item, email: item.email || email } as RestaurantUser; + } + } + } catch (error) { + console.error("[Auth] Error searching by email:", error); + } + return null; + }; + useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { try { if (currentUser && currentUser.email) { setFirebaseUser(currentUser); + + let data: RestaurantUser | null = null; + + // Strategy 1: Try building document ID from email (website format) const docId = buildDocumentId(currentUser.email); const userRef = ref(db, `Restaurantes/${docId}`); - const snapshot = await get(userRef); + let snapshot = await get(userRef); if (snapshot.exists()) { - const data = snapshot.val() as RestaurantUser; + data = snapshot.val() as RestaurantUser; + } + + // Strategy 2: Try with original email as document ID (Android format) + if (!data) { + const originalRef = ref(db, `Restaurantes/${currentUser.email}`); + snapshot = await get(originalRef); + if (snapshot.exists()) { + data = snapshot.val() as RestaurantUser; + } + } + + // Strategy 3: Search all Restaurantes documents by UID + if (!data && currentUser.uid) { + data = await findUserByUid(currentUser.uid, currentUser); + } + + // Strategy 4: Search all Restaurantes documents by email + if (!data) { + data = await findUserByEmail(currentUser.email); + } + + if (data) { if (data.accountType === "ESTABELECIMENTO") { setUser(data); } else { - console.warn("User is not a restaurant"); + console.warn("[Auth] User is not a restaurant account type:", data.accountType); setUser(null); await signOut(auth); } } else { - console.warn("User record not found in Restaurantes"); + // User exists in Auth but not in database — possible orphaned account + console.warn("[Auth] User record not found in Restaurantes for:", currentUser.email); + console.warn("[Auth] UID:", currentUser.uid); setUser(null); } } else { + // No Firebase user — clear state setFirebaseUser(null); setUser(null); } } catch (error) { - console.error("Auth initialization error:", error); + console.error("[Auth] Error in onAuthStateChanged:", error); + // On error, clear user state to prevent broken state + setUser(null); + setFirebaseUser(null); } finally { setLoading(false); } diff --git a/reserva-mesa-dashboard/hooks/useMesas.ts b/reserva-mesa-dashboard/hooks/useMesas.ts index 1851552..93abdd6 100644 --- a/reserva-mesa-dashboard/hooks/useMesas.ts +++ b/reserva-mesa-dashboard/hooks/useMesas.ts @@ -17,26 +17,32 @@ export function useMesas() { const mesasRef = ref(db, "Mesas"); const unsubscribe = onValue(mesasRef, (snapshot) => { - const data = snapshot.val(); - const list: Mesa[] = []; - - if (data) { - Object.keys(data).forEach((key) => { - const item = data[key]; - if (item.restauranteEmail === user.email) { - list.push({ - id: key, - ...item - }); - } - }); - } - - // Sort by table number - list.sort((a, b) => a.numero - b.numero); + try { + const data = snapshot.val(); + const list: Mesa[] = []; + + if (data) { + Object.keys(data).forEach((key) => { + const item = data[key]; + if (item?.restauranteEmail === user.email) { + list.push({ + id: key, + ...item + }); + } + }); + } + + // Sort by table number + list.sort((a, b) => a.numero - b.numero); - setMesas(list); - setLoading(false); + setMesas(list); + } catch (error) { + console.error("[useMesas] Error processing data:", error); + setMesas([]); + } finally { + setLoading(false); + } }); return () => off(mesasRef, "value", unsubscribe); diff --git a/reserva-mesa-dashboard/hooks/useReservas.ts b/reserva-mesa-dashboard/hooks/useReservas.ts index 48a573a..0aeaf98 100644 --- a/reserva-mesa-dashboard/hooks/useReservas.ts +++ b/reserva-mesa-dashboard/hooks/useReservas.ts @@ -17,30 +17,36 @@ export function useReservas() { const reservasRef = ref(db, "reservas"); const unsubscribe = onValue(reservasRef, (snapshot) => { - const data = snapshot.val(); - const list: Reserva[] = []; - - if (data) { - Object.keys(data).forEach((key) => { - const item = data[key]; - if (item.restauranteEmail === user.email) { - list.push({ - id: key, - ...item - }); - } + try { + const data = snapshot.val(); + const list: Reserva[] = []; + + if (data) { + Object.keys(data).forEach((key) => { + const item = data[key]; + if (item?.restauranteEmail === user.email) { + list.push({ + id: key, + ...item + }); + } + }); + } + + // Sort by date and time (newest first for management) + list.sort((a, b) => { + const dateA = new Date(`${a.data.replace(/-/g, "/")} ${a.hora}`); + const dateB = new Date(`${b.data.replace(/-/g, "/")} ${b.hora}`); + return dateB.getTime() - dateA.getTime(); }); - } - - // Sort by date and time (newest first for management) - list.sort((a, b) => { - const dateA = new Date(`${a.data.replace(/-/g, "/")} ${a.hora}`); - const dateB = new Date(`${b.data.replace(/-/g, "/")} ${b.hora}`); - return dateB.getTime() - dateA.getTime(); - }); - setReservas(list); - setLoading(false); + setReservas(list); + } catch (error) { + console.error("[useReservas] Error processing data:", error); + setReservas([]); + } finally { + setLoading(false); + } }); return () => off(reservasRef, "value", unsubscribe); diff --git a/reserva-mesa-dashboard/hooks/useStaff.ts b/reserva-mesa-dashboard/hooks/useStaff.ts index 1bb327d..864cf54 100644 --- a/reserva-mesa-dashboard/hooks/useStaff.ts +++ b/reserva-mesa-dashboard/hooks/useStaff.ts @@ -17,23 +17,29 @@ export function useStaff() { const staffRef = ref(db, "Staff"); const unsubscribe = onValue(staffRef, (snapshot) => { - const data = snapshot.val(); - const list: Staff[] = []; - - if (data) { - Object.keys(data).forEach((key) => { - const item = data[key]; - if (item.restauranteEmail === user.email) { - list.push({ - id: key, - ...item - }); - } - }); + try { + const data = snapshot.val(); + const list: Staff[] = []; + + if (data) { + Object.keys(data).forEach((key) => { + const item = data[key]; + if (item?.restauranteEmail === user.email) { + list.push({ + id: key, + ...item + }); + } + }); + } + + setStaff(list); + } catch (error) { + console.error("[useStaff] Error processing data:", error); + setStaff([]); + } finally { + setLoading(false); } - - setStaff(list); - setLoading(false); }); return () => off(staffRef, "value", unsubscribe); diff --git a/reserva-mesa-dashboard/lib/firebase.ts b/reserva-mesa-dashboard/lib/firebase.ts index f743d8f..4ee3470 100644 --- a/reserva-mesa-dashboard/lib/firebase.ts +++ b/reserva-mesa-dashboard/lib/firebase.ts @@ -1,22 +1,53 @@ -import { initializeApp, getApps } from "firebase/app"; +import { initializeApp, getApps, getApp } from "firebase/app"; import { getAuth } from "firebase/auth"; import { getDatabase } from "firebase/database"; // As variáveis de ambiente devem ser configuradas no Vercel e no ficheiro .env.local const firebaseConfig = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || "AIzaSyCPz7Pd3tJj3QkF7fV_vudCJythNsyR57k", - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || "namesa-429c1.firebaseapp.com", - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || "namesa-429c1", - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || "namesa-429c1.firebasestorage.app", - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || "476421715902", - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "1:476421715902:web:placeholder", // placeholder needed for web client SDK - // Nota importante: Como verificado na codebase Android, - // O ReservaMesa usa Realtime Database e não Firestore. + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL || "https://namesa-429c1-default-rtdb.firebaseio.com" }; -// Initialize Firebase only if there are no apps initialized yet -const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; +// Validate that all required environment variables are present +const requiredEnvVars = [ + "NEXT_PUBLIC_FIREBASE_API_KEY", + "NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN", + "NEXT_PUBLIC_FIREBASE_PROJECT_ID", + "NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET", + "NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID", + "NEXT_PUBLIC_FIREBASE_APP_ID", +]; + +const missingVars = requiredEnvVars.filter( + (key) => !process.env[key] +); + +if (missingVars.length > 0) { + console.error( + "[Firebase] Missing environment variables:", + missingVars.join(", "), + "\nPlease ensure all Firebase environment variables are set in .env.local or your hosting platform." + ); +} + +// Initialize Firebase only once — prevents duplicate app errors in development (HMR) and SSR +let app; +try { + app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp(); +} catch (error: any) { + if (error?.code === "app/duplicate-app") { + app = getApp(); + } else { + throw new Error( + `[Firebase] Failed to initialize: ${error?.message || "Unknown error"}. Check your environment variables.` + ); + } +} export const auth = getAuth(app); export const db = getDatabase(app); diff --git a/reserva-mesa-dashboard/package-lock.json b/reserva-mesa-dashboard/package-lock.json index 2caa625..d4764ed 100644 --- a/reserva-mesa-dashboard/package-lock.json +++ b/reserva-mesa-dashboard/package-lock.json @@ -14,7 +14,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", - "firebase": "^10.12.0", + "firebase": "^10.14.1", "framer-motion": "^11.1.7", "lucide-react": "^0.378.0", "next": "14.2.3", diff --git a/reserva-mesa-dashboard/package.json b/reserva-mesa-dashboard/package.json index 35f89a5..6f16048 100644 --- a/reserva-mesa-dashboard/package.json +++ b/reserva-mesa-dashboard/package.json @@ -10,10 +10,12 @@ }, "dependencies": { "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", - "firebase": "^10.12.0", + "firebase": "^10.14.1", "framer-motion": "^11.1.7", "lucide-react": "^0.378.0", "next": "14.2.3", @@ -23,8 +25,6 @@ "recharts": "^2.12.7", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-slot": "^1.0.2", "zod": "^3.23.6" }, "devDependencies": {