209 lines
7.1 KiB
Markdown
209 lines
7.1 KiB
Markdown
# 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<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.
|
|
|
|
```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 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_`)
|
|
|
|
```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
|
|
}
|
|
}
|
|
```
|