aboutsummaryrefslogtreecommitdiff
path: root/api/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/plugins')
-rw-r--r--api/src/plugins/auth.plugin.ts93
-rw-r--r--api/src/plugins/db.plugin.ts31
-rw-r--r--api/src/plugins/events.plugin.ts34
-rw-r--r--api/src/plugins/events.routes.ts58
4 files changed, 216 insertions, 0 deletions
diff --git a/api/src/plugins/auth.plugin.ts b/api/src/plugins/auth.plugin.ts
new file mode 100644
index 0000000..0e6718a
--- /dev/null
+++ b/api/src/plugins/auth.plugin.ts
@@ -0,0 +1,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',
+});
diff --git a/api/src/plugins/db.plugin.ts b/api/src/plugins/db.plugin.ts
new file mode 100644
index 0000000..47c82dd
--- /dev/null
+++ b/api/src/plugins/db.plugin.ts
@@ -0,0 +1,31 @@
+/**
+ * db.plugin.ts
+ *
+ * Plugin Fastify que disponibiliza o banco de dados como decorador.
+ *
+ * Após registrar este plugin, qualquer rota pode acessar o banco via:
+ * fastify.db ou request.server.db
+ *
+ * O uso de fastify-plugin (fp) é essencial: sem ele, o Fastify encapsularia
+ * o decorador apenas no escopo do plugin, tornando-o invisível para outros plugins.
+ */
+
+import fp from 'fastify-plugin';
+import type { FastifyInstance } from 'fastify';
+import { db, type Database } from '@/db/connection.js';
+
+// Augmenta a tipagem do Fastify para incluir `db` com type-safety completo
+declare module 'fastify' {
+ interface FastifyInstance {
+ db: Database;
+ }
+}
+
+async function dbPlugin(fastify: FastifyInstance): Promise<void> {
+ fastify.decorate('db', db);
+}
+
+export default fp(dbPlugin, {
+ name: 'db-plugin',
+ fastify: '4.x',
+});
diff --git a/api/src/plugins/events.plugin.ts b/api/src/plugins/events.plugin.ts
new file mode 100644
index 0000000..fdf0e74
--- /dev/null
+++ b/api/src/plugins/events.plugin.ts
@@ -0,0 +1,34 @@
+/**
+ * events.plugin.ts
+ *
+ * Expõe um EventEmitter global no fastify para pub/sub em memória.
+ * Ideal para disparar Server-Sent Events (SSE) aos clientes conectados.
+ */
+
+import fp from 'fastify-plugin';
+import { EventEmitter } from 'node:events';
+import type { FastifyInstance } from 'fastify';
+
+export interface AppEventMap {
+ 'ticket.created': { id: number; title: string; creatorId: number };
+ 'ticket.updated': { id: number; status: string; assigneeId: number | null; creatorId: number; authorId: number };
+ 'comment.added': { ticketId: number; authorId: number; isInternal: boolean; creatorId: number; assigneeId: number | null };
+}
+
+declare module 'fastify' {
+ interface FastifyInstance {
+ events: EventEmitter;
+ }
+}
+
+async function eventsPlugin(fastify: FastifyInstance): Promise<void> {
+ const emitter = new EventEmitter();
+ // Permite muitos clientes SSE conectados simultaneamente
+ emitter.setMaxListeners(100);
+
+ fastify.decorate('events', emitter);
+}
+
+export default fp(eventsPlugin, {
+ name: 'events-plugin',
+});
diff --git a/api/src/plugins/events.routes.ts b/api/src/plugins/events.routes.ts
new file mode 100644
index 0000000..fd14732
--- /dev/null
+++ b/api/src/plugins/events.routes.ts
@@ -0,0 +1,58 @@
+/**
+ * events.routes.ts
+ *
+ * Endpoint SSE (Server-Sent Events).
+ * Mantém a conexão aberta e repassa eventos do `fastify.events` para o cliente.
+ */
+
+import fp from 'fastify-plugin';
+import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
+
+async function eventsRoutes(fastify: FastifyInstance): Promise<void> {
+ fastify.get('/', {
+ schema: {
+ tags: ['Dashboard'],
+ summary: 'Stream de eventos em tempo real (SSE)',
+ },
+ preHandler: [fastify.authenticate],
+ handler: (request: FastifyRequest, reply: FastifyReply) => {
+ // 1. Configura os headers para SSE
+ reply.raw.writeHead(200, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive',
+ });
+ reply.raw.write('\n'); // Garante envio do header imediato
+
+ // Helper para formatar pacote SSE
+ const sendEvent = (event: string, data: any) => {
+ reply.raw.write(`event: ${event}\n`);
+ reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
+ };
+
+ // 2. Registra os listeners
+ const onTicketCreated = (data: any) => sendEvent('ticket.created', data);
+ const onTicketUpdated = (data: any) => sendEvent('ticket.updated', data);
+ const onCommentAdded = (data: any) => sendEvent('comment.added', data);
+
+ fastify.events.on('ticket.created', onTicketCreated);
+ fastify.events.on('ticket.updated', onTicketUpdated);
+ fastify.events.on('comment.added', onCommentAdded);
+
+ // Ping a cada 30s para manter a conexão ativa (evitar timeout do LB/Nginx)
+ const pingInterval = setInterval(() => {
+ reply.raw.write(':\n\n'); // SSE comment block serve como heartbeat
+ }, 30000);
+
+ // 3. Limpeza quando o cliente desconecta
+ request.raw.on('close', () => {
+ clearInterval(pingInterval);
+ fastify.events.off('ticket.created', onTicketCreated);
+ fastify.events.off('ticket.updated', onTicketUpdated);
+ fastify.events.off('comment.added', onCommentAdded);
+ });
+ },
+ });
+}
+
+export default eventsRoutes;