From 2d1f09154fd53796c016aa4edc57ed1c05b9bb29 Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Wed, 21 Jan 2026 09:52:34 +0000 Subject: [PATCH] Add RequireAuth and events module for user authentication and event management --- web/src/App.tsx | 27 ++---- web/src/components/auth/RequireAuth.tsx | 20 +++++ web/src/lib/auth.ts | 13 +-- web/src/lib/events.ts | 26 ++++++ web/src/pages/AuthLogin.tsx | 87 +++++++++--------- web/src/pages/Profile.tsx | 115 +++++++++++++++++++----- web/src/routes.tsx | 67 ++++++++++---- 7 files changed, 242 insertions(+), 113 deletions(-) create mode 100644 web/src/components/auth/RequireAuth.tsx create mode 100644 web/src/lib/events.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index cad4db6..4e4b9ec 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,23 +1,6 @@ -import { RouterProvider } from 'react-router-dom'; -import { router } from './routes'; - -const App = () => ; -import { useEffect } from 'react' -import { signIn } from './lib/auth' - -useEffect(() => { - ;(async () => { - try { - await signIn('TEU_EMAIL', 'TUA_PASSWORD') - console.log('LOGIN OK') - } catch (e: any) { - console.log('LOGIN ERRO:', e.message) - } - })() -}, []) - - -export default App; - - +import { RouterProvider } from 'react-router-dom' +import { router } from './routes' +export default function App() { + return +} diff --git a/web/src/components/auth/RequireAuth.tsx b/web/src/components/auth/RequireAuth.tsx new file mode 100644 index 0000000..86e4636 --- /dev/null +++ b/web/src/components/auth/RequireAuth.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react' +import { Navigate } from 'react-router-dom' +import { getUser } from '../../lib/auth' + +export function RequireAuth({ children }: { children: React.ReactNode }) { + const [loading, setLoading] = useState(true) + const [ok, setOk] = useState(false) + + useEffect(() => { + ;(async () => { + const user = await getUser() + setOk(!!user) + setLoading(false) + })() + }, []) + + if (loading) return null + if (!ok) return + return <>{children} +} diff --git a/web/src/lib/auth.ts b/web/src/lib/auth.ts index d61383e..d35c4b6 100644 --- a/web/src/lib/auth.ts +++ b/web/src/lib/auth.ts @@ -1,19 +1,13 @@ import { supabase } from './supabase' export async function signIn(email: string, password: string) { - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password, - }) + const { data, error } = await supabase.auth.signInWithPassword({ email, password }) if (error) throw error return data } export async function signUp(email: string, password: string) { - const { data, error } = await supabase.auth.signUp({ - email, - password, - }) + const { data, error } = await supabase.auth.signUp({ email, password }) if (error) throw error return data } @@ -24,6 +18,7 @@ export async function signOut() { } export async function getUser() { - const { data } = await supabase.auth.getUser() + const { data, error } = await supabase.auth.getUser() + if (error) return null return data.user } diff --git a/web/src/lib/events.ts b/web/src/lib/events.ts new file mode 100644 index 0000000..0a313e2 --- /dev/null +++ b/web/src/lib/events.ts @@ -0,0 +1,26 @@ +import { supabase } from './supabase' +import { getUser } from './auth' + +export type EventRow = { + id: number + title: string + description: string | null + date: string + user_id: string +} + +// LISTAR eventos do utilizador logado +export async function listEvents() { + const user = await getUser() + if (!user) { + throw new Error('Utilizador não autenticado') + } + + const { data, error } = await supabase + .from('events') + .select('*') + .order('date', { ascending: true }) + + if (error) throw error + return (data ?? []) as EventRow[] +} diff --git a/web/src/pages/AuthLogin.tsx b/web/src/pages/AuthLogin.tsx index afb020c..c8d411c 100644 --- a/web/src/pages/AuthLogin.tsx +++ b/web/src/pages/AuthLogin.tsx @@ -1,34 +1,38 @@ -import { FormEvent, useEffect, useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { Input } from '../components/ui/input'; -import { Button } from '../components/ui/button'; -import { Card } from '../components/ui/card'; -import { useApp } from '../context/AppContext'; -import { LogIn, Mail, Lock } from 'lucide-react'; +import { FormEvent, useEffect, useState } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import { Input } from '../components/ui/input' +import { Button } from '../components/ui/button' +import { Card } from '../components/ui/card' +import { LogIn } from 'lucide-react' +import { signIn, getUser } from '../lib/auth' export default function AuthLogin() { - const [email, setEmail] = useState('cliente@demo.com'); - const [password, setPassword] = useState('123'); - const [error, setError] = useState(''); - const { login, user } = useApp(); - const navigate = useNavigate(); + const [email, setEmail] = useState('cliente@demo.com') + const [password, setPassword] = useState('123') + const [error, setError] = useState('') + const navigate = useNavigate() + // Se já estiver logado, redireciona useEffect(() => { - if (!user) return; - const target = user.role === 'barbearia' ? '/painel' : '/explorar'; - navigate(target, { replace: true }); - }, [user, navigate]); + ;(async () => { + const user = await getUser() + if (user) { + navigate('/explorar', { replace: true }) + } + })() + }, [navigate]) - const onSubmit = (e: FormEvent) => { - e.preventDefault(); - const ok = login(email, password); - if (!ok) { - setError('Credenciais inválidas'); - } else { - const target = user?.role === 'barbearia' ? '/painel' : '/explorar'; - navigate(target); + async function handleLogin(e: FormEvent) { + e.preventDefault() + setError('') + + try { + await signIn(email, password) + navigate('/explorar', { replace: true }) + } catch { + setError('Credenciais inválidas') } - }; + } return (
@@ -37,8 +41,12 @@ export default function AuthLogin() {
-

Bem-vindo de volta

-

Entre na sua conta para continuar

+

+ Bem-vindo de volta +

+

+ Entre na sua conta para continuar +

@@ -47,31 +55,32 @@ export default function AuthLogin() {

Barbearia: barber@demo.com / 123

-
+ { - setEmail(e.target.value); - setError(''); + setEmail(e.target.value) + setError('') }} required - error={error ? undefined : undefined} placeholder="seu@email.com" /> + { - setPassword(e.target.value); - setError(''); + setPassword(e.target.value) + setError('') }} required error={error} placeholder="••••••••" /> + @@ -80,17 +89,15 @@ export default function AuthLogin() {

Não tem conta?{' '} - + Criar conta

- ); + ) } - - - - - diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx index 55474b0..57934a9 100644 --- a/web/src/pages/Profile.tsx +++ b/web/src/pages/Profile.tsx @@ -1,35 +1,57 @@ -import { Card } from '../components/ui/card'; -import { Badge } from '../components/ui/badge'; -import { currency } from '../lib/format'; -import { useApp } from '../context/AppContext'; -import { Calendar, ShoppingBag, User, Clock } from 'lucide-react'; +import { useEffect, useState } from 'react' +import { Card } from '../components/ui/card' +import { Badge } from '../components/ui/badge' +import { currency } from '../lib/format' +import { useApp } from '../context/AppContext' +import { Calendar, ShoppingBag, User, Clock } from 'lucide-react' +import { listEvents, type EventRow } from '../lib/events' const statusColor: Record = { pendente: 'amber', confirmado: 'green', concluido: 'green', cancelado: 'red', -}; +} const statusLabel: Record = { pendente: 'Pendente', confirmado: 'Confirmado', concluido: 'Concluído', cancelado: 'Cancelado', -}; +} export default function Profile() { - const { user, appointments, orders, shops } = useApp(); + const { user, appointments, orders, shops } = useApp() + + // ✅ Supabase events + const [events, setEvents] = useState([]) + const [loadingEvents, setLoadingEvents] = useState(true) + const [eventsError, setEventsError] = useState('') + + useEffect(() => { + ;(async () => { + try { + setEventsError('') + const data = await listEvents() + setEvents(data) + } catch (e: any) { + setEventsError(e.message || 'Erro ao carregar eventos') + } finally { + setLoadingEvents(false) + } + })() + }, []) + if (!user) { return (

Faça login para ver o perfil.

- ); + ) } - const myAppointments = appointments.filter((a) => a.customerId === user.id); - const myOrders = orders.filter((o) => o.customerId === user.id); + const myAppointments = appointments.filter((a) => a.customerId === user.id) + const myOrders = orders.filter((o) => o.customerId === user.id) return (
@@ -49,6 +71,56 @@ export default function Profile() {
+ {/* ✅ Supabase Events Section */} +
+
+ +

Eventos (Supabase)

+ + {loadingEvents ? '…' : events.length} + +
+ + {loadingEvents ? ( + +

A carregar eventos…

+
+ ) : eventsError ? ( + +

{eventsError}

+

+ Confirma que estás logado e que as policies RLS estão corretas. +

+
+ ) : events.length === 0 ? ( + + +

Nenhum evento ainda

+

+ Cria um evento para aparecer aqui. +

+
+ ) : ( +
+ {events.map((ev) => ( + +
+
+

{ev.title}

+ {ev.description && ( +

{ev.description}

+ )} +

+ {new Date(ev.date).toLocaleString()} +

+
+
+
+ ))} +
+ )} +
+ {/* Appointments Section */}
@@ -65,8 +137,8 @@ export default function Profile() { ) : (
{myAppointments.map((a) => { - const shop = shops.find((s) => s.id === a.shopId); - const service = shop?.services.find((s) => s.id === a.serviceId); + const shop = shops.find((s) => s.id === a.shopId) + const service = shop?.services.find((s) => s.id === a.serviceId) return (
@@ -90,7 +162,7 @@ export default function Profile() {
- ); + ) })}
)} @@ -112,7 +184,7 @@ export default function Profile() { ) : (
{myOrders.map((o) => { - const shop = shops.find((s) => s.id === o.shopId); + const shop = shops.find((s) => s.id === o.shopId) return (
@@ -132,23 +204,20 @@ export default function Profile() { minute: '2-digit', })}

-

{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}

+

+ {o.items.length} {o.items.length === 1 ? 'item' : 'itens'} +

{currency(o.total)}

- ); + ) })} )}
- ); + ) } - - - - - diff --git a/web/src/routes.tsx b/web/src/routes.tsx index 5256a98..77cc04b 100644 --- a/web/src/routes.tsx +++ b/web/src/routes.tsx @@ -1,14 +1,16 @@ -import { createBrowserRouter } from 'react-router-dom'; -import { Shell } from './components/layout/Shell'; -import Landing from './pages/Landing'; -import AuthLogin from './pages/AuthLogin'; -import AuthRegister from './pages/AuthRegister'; -import Explore from './pages/Explore'; -import ShopDetails from './pages/ShopDetails'; -import Booking from './pages/Booking'; -import Cart from './pages/Cart'; -import Profile from './pages/Profile'; -import Dashboard from './pages/Dashboard'; +import { createBrowserRouter } from 'react-router-dom' +import { Shell } from './components/layout/Shell' +import { RequireAuth } from './components/auth/RequireAuth' + +import Landing from './pages/Landing' +import AuthLogin from './pages/AuthLogin' +import AuthRegister from './pages/AuthRegister' +import Explore from './pages/Explore' +import ShopDetails from './pages/ShopDetails' +import Booking from './pages/Booking' +import Cart from './pages/Cart' +import Profile from './pages/Profile' +import Dashboard from './pages/Dashboard' export const router = createBrowserRouter([ { @@ -19,13 +21,40 @@ export const router = createBrowserRouter([ { path: '/registo', element: }, { path: '/explorar', element: }, { path: '/barbearia/:id', element: }, - { path: '/agendar/:id', element: }, - { path: '/carrinho', element: }, - { path: '/perfil', element: }, - { path: '/painel', element: }, + + // ✅ PROTEGIDAS (precisam de login) + { + path: '/agendar/:id', + element: ( + + + + ), + }, + { + path: '/carrinho', + element: ( + + + + ), + }, + { + path: '/perfil', + element: ( + + + + ), + }, + { + path: '/painel', + element: ( + + + + ), + }, ], }, -]); - - - +])