Files
petlink_final/docs/07-seguranca.md
2026-05-04 09:43:36 +01:00

7.1 KiB

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.

// lib/auth/password.ts
import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12;

export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

export async function verifyPassword(password: string, hash: string): Promise<boolean> {
  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.

// 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:

// 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

// 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:

// 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

// 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 PawLink
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 PawLink — 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_)
// 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
  }
}