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 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 (chavessk_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
}
}