first commit
This commit is contained in:
208
docs/07-seguranca.md
Normal file
208
docs/07-seguranca.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 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
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user