# 7. Segurança e Conformidade ## 7.1 Autenticação e Autorização ### Palavras-passe As palavras-passe são **nunca armazenadas em texto simples**. Utiliza-se o algoritmo bcrypt com factor de custo 12, que torna ataques de força bruta computacionalmente inviáveis mesmo com hardware moderno. ```typescript // lib/auth/password.ts import bcrypt from 'bcryptjs'; const SALT_ROUNDS = 12; export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); } export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } ``` ### Sessões e JWT As sessões são geridas pelo NextAuth.js com tokens JWT assinados com um segredo de 256 bits armazenado em variável de ambiente. O token expira ao fim de 30 dias. O middleware Next.js intercepta todas as rotas protegidas antes de qualquer lógica de negócio. ```typescript // middleware.ts import { withAuth } from 'next-auth/middleware'; export default withAuth({ pages: { signIn: '/login' } }); export const config = { matcher: [ '/account/:path*', '/donate/:path*', '/dashboard/:path*', ] }; ``` ### Verificação de Idade A verificação de +18 anos é **sempre feita no servidor**, nunca confiando em dados do cliente: ```typescript // lib/auth/age-validation.ts export function isAdult(birthdate: Date): boolean { const today = new Date(); const age = today.getFullYear() - birthdate.getFullYear(); const monthDiff = today.getMonth() - birthdate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthdate.getDate())) { return age - 1 >= 18; } return age >= 18; } ``` ### Controlo de Acesso por Roles | Role | Permissões | |---|---| | `USER` | Adoptar animais, fazer doações, gerir a própria conta | | `SHELTER_ADMIN` | Tudo de USER + gerir animais/reservas/necessidades do seu canil | | `ADMIN` | Acesso total à plataforma, moderação, estatísticas | --- ## 7.2 Segurança da Aplicação ### Headers de Segurança HTTP ```typescript // next.config.ts const securityHeaders = [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin' }, { key: 'Content-Security-Policy', value: [ "default-src 'self'", "script-src 'self' 'unsafe-inline' https://js.stripe.com", "frame-src https://js.stripe.com", "img-src 'self' data: https://*.supabase.co", "connect-src 'self' https://api.anthropic.com https://api.stripe.com", ].join('; ') }, ]; ``` ### Validação de Inputs Todos os dados recebidos nas API Routes são validados com Zod antes de qualquer processamento: ```typescript // app/api/reservations/route.ts import { z } from 'zod'; const ReservationSchema = z.object({ animalId: z.string().cuid(), date: z.string().datetime().refine( date => new Date(date) > new Date(), { message: 'A data deve ser no futuro' } ), notes: z.string().max(500).optional(), }); export async function POST(req: Request) { const body = await req.json(); const parsed = ReservationSchema.safeParse(body); if (!parsed.success) { return Response.json({ error: parsed.error.issues }, { status: 400 }); } // ... lógica de negócio com parsed.data (type-safe) } ``` ### Rate Limiting ```typescript // lib/rate-limit.ts — protege endpoints de autenticação import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; export const authRateLimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 tentativas por minuto por IP }); ``` ### Uploads de Imagens Todos os uploads são validados antes de armazenar no Supabase Storage: - Tipo MIME verificado no servidor (não confia no Content-Type do cliente) - Tamanho máximo: 5MB por ficheiro - Extensões permitidas: `.jpg`, `.jpeg`, `.png`, `.webp` - Nome do ficheiro gerado pelo servidor (nunca usa o nome original do utilizador) --- ## 7.3 Conformidade com o RGPD | Princípio RGPD | Implementação na PetLink | |---|---| | **Consentimento explícito** | Checkbox obrigatório no registo com link para Política de Privacidade. Consentimento separado para emails de marketing (opcional). | | **Finalidade limitada** | Dados pessoais usados apenas para adopção e doação — não partilhados com terceiros excepto processadores necessários (Stripe, Resend). | | **Minimização de dados** | Apenas nome, email, distrito, data de nascimento e palavra-passe são recolhidos no registo. | | **Exactidão** | Utilizador pode actualizar todos os seus dados nas Definições a qualquer momento. | | **Limitação de armazenamento** | Dados de contas inactivas (sem login há 24+ meses) são anonimizados automaticamente. | | **Direito ao apagamento** | Botão "Eliminar Conta" nas Definições — inicia processo de apagamento de dados pessoais em 30 dias. | | **Portabilidade** | Exportação dos dados do utilizador em formato JSON nas Definições. | | **Notificação de violações** | Plano de resposta a incidentes com notificação à CNPD em 72 horas conforme legislação. | | **Processadores de dados** | Contratos de processamento de dados (DPA) celebrados com Stripe, Resend, Supabase e Vercel. | ### Política de Cookies | Cookie | Tipo | Finalidade | Duração | |---|---|---|---| | `next-auth.session-token` | Necessário | Sessão de autenticação | 30 dias | | `next-auth.csrf-token` | Necessário | Protecção CSRF | Sessão | | `theme` | Preferências | Modo claro/escuro | 1 ano | | Analytics Vercel | Analítico | Métricas de performance anonimizadas | Sessão | Não são usados cookies de rastreamento ou publicidade. --- ## 7.4 Segurança dos Pagamentos - Dados de cartão **nunca passam pelos servidores da PetLink** — tratados directamente pelo Stripe via Payment Element - Stripe é certificado **PCI DSS Level 1** — o mais elevado nível de conformidade - Webhooks Stripe verificados com **assinatura HMAC-SHA256** — previne falsificação de eventos de pagamento - Montantes validados no servidor antes de criar PaymentIntent — cliente não pode alterar o valor - Ambiente de teste (chaves `sk_test_`) separado do ambiente de produção (chaves `sk_live_`) ```typescript // app/api/payments/webhook/route.ts import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const body = await req.text(); const signature = req.headers.get('stripe-signature')!; let event: Stripe.Event; try { // Verifica assinatura HMAC — rejeitado se não for do Stripe event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!); } catch { return new Response('Webhook signature verification failed', { status: 400 }); } // Processar evento verificado if (event.type === 'payment_intent.succeeded') { // Actualizar donation status na base de dados // Enviar email de recibo } } ```