aboutsummaryrefslogtreecommitdiff
path: root/api/src/plugins/auth.plugin.ts
blob: 0e6718ae761ddf02d06f9965cc30f2b57340237c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * auth.plugin.ts
 *
 * Registra o @fastify/jwt e expõe dois decoradores:
 *   fastify.authenticate        → rejeita requests sem token válido (401)
 *   fastify.optionalAuthenticate → injeta o usuário se houver token, mas não falha
 *
 * Uso nas rotas:
 *   { preHandler: [fastify.authenticate] }
 */

import fp from 'fastify-plugin';
import jwt from '@fastify/jwt';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

// ─── Payload do token ─────────────────────────────────────────────────────────

export interface JwtPayload {
  id:    number;
  email: string;
  role:  'requester' | 'agent' | 'admin';
}

// ─── Augmentação de tipos Fastify ─────────────────────────────────────────────

declare module 'fastify' {
  interface FastifyInstance {
    authenticate:         (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
    optionalAuthenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
  }
  interface FastifyRequest {
    user: JwtPayload;
  }
}

declare module '@fastify/jwt' {
  interface FastifyJWT {
    payload: JwtPayload;
    user:    JwtPayload;
  }
}

// ─── Plugin ───────────────────────────────────────────────────────────────────

async function authPlugin(fastify: FastifyInstance): Promise<void> {
  const secret = process.env['JWT_SECRET'];
  if (!secret) {
    throw new Error('JWT_SECRET não definido. Verifique o arquivo .env');
  }

  await fastify.register(jwt, {
    secret,
    sign: {
      expiresIn: process.env['JWT_EXPIRES_IN'] ?? '7d',
    },
  });

  // ── authenticate ────────────────────────────────────────────────────────────
  // Exige token válido — retorna 401 se ausente ou expirado
  fastify.decorate(
    'authenticate',
    async (request: FastifyRequest, reply: FastifyReply) => {
      try {
        // Suporte para SSE (EventSource não envia headers, envia via query string)
        const query = request.query as { token?: string };
        if (query.token && !request.headers.authorization) {
          request.headers.authorization = `Bearer ${query.token}`;
        }
        await request.jwtVerify();
      } catch {
        await reply.unauthorized('Token inválido ou expirado. Faça login novamente.');
      }
    },
  );

  // ── optionalAuthenticate ────────────────────────────────────────────────────
  // Tenta verificar o token mas não falha se ausente
  fastify.decorate(
    'optionalAuthenticate',
    async (request: FastifyRequest, _reply: FastifyReply) => {
      try {
        await request.jwtVerify();
      } catch {
        // Sem token → request.user fica undefined — trate no controller
      }
    },
  );
}

export default fp(authPlugin, {
  name: 'auth-plugin',
  fastify: '4.x',
});