diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
| commit | ef55d9bb24430610b60c1b77821307cd72c08c55 (patch) | |
| tree | e6935a810b34370b0a4c821e55f6397aac50c832 /api/src/plugins | |
| download | helpdesk-master.tar.gz helpdesk-master.zip | |
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'api/src/plugins')
| -rw-r--r-- | api/src/plugins/auth.plugin.ts | 93 | ||||
| -rw-r--r-- | api/src/plugins/db.plugin.ts | 31 | ||||
| -rw-r--r-- | api/src/plugins/events.plugin.ts | 34 | ||||
| -rw-r--r-- | api/src/plugins/events.routes.ts | 58 |
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; |