1.1.0 • Published 1 month ago

@shield-acl/fastify v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 month ago

@shield-acl/fastify

Características

  • 🚀 Plugin Fastify Nativo: Integração perfeita com Fastify 4.x e 5.x
  • 🛡️ Middleware de Autorização: Proteja rotas com verificações declarativas
  • 🎯 Suporte a DDD: Guards para Use Cases com autorização integrada
  • 🔌 Decorators Úteis: request.can(), fastify.authorize() e mais
  • 📝 TypeScript First: Totalmente tipado com IntelliSense completo
  • Performance: Otimizado para alta performance com cache

Instalação

npm install @shield-acl/fastify @shield-acl/core
# ou
yarn add @shield-acl/fastify @shield-acl/core
# ou
pnpm add @shield-acl/fastify @shield-acl/core

Uso Rápido

1. Registrar o Plugin

import Fastify from 'fastify'
import { ACL } from '@shield-acl/core'
import shieldACLPlugin from '@shield-acl/fastify'

const fastify = Fastify()
const acl = new ACL()

// Definir roles
acl.defineRole({
  name: 'admin',
  permissions: [{ action: '*', resource: '*' }]
})

acl.defineRole({
  name: 'user',
  permissions: [
    { action: 'read', resource: 'posts' },
    { action: 'create', resource: 'posts' }
  ]
})

// Registrar plugin
await fastify.register(shieldACLPlugin, {
  acl,
  getUserFromRequest: async (request) => {
    // Extrair usuário do JWT, sessão, etc
    const token = request.headers.authorization?.replace('Bearer ', '')
    if (!token) return null
    
    // Decodificar token e retornar usuário
    const decoded = await verifyToken(token)
    return {
      id: decoded.sub,
      roles: decoded.roles,
      attributes: decoded.attributes
    }
  }
})

2. Proteger Rotas

// Usando preHandler
fastify.get('/admin', {
  preHandler: fastify.authorize({
    action: 'admin',
    resource: '*'
  })
}, async (request, reply) => {
  return { message: 'Welcome admin!' }
})

// Múltiplas verificações
fastify.post('/posts', {
  preHandler: [
    fastify.authenticate(), // seu middleware de autenticação
    fastify.authorize({
      action: 'create',
      resource: 'posts'
    })
  ]
}, async (request, reply) => {
  return { post: 'created' }
})

// Com recurso dinâmico
fastify.put('/posts/:id', {
  preHandler: fastify.authorize({
    action: 'update',
    resource: (request) => `posts:${request.params.id}`,
    context: async (request) => ({
      post: await getPost(request.params.id)
    })
  })
}, async (request, reply) => {
  return { updated: true }
})

3. Verificações Programáticas

fastify.get('/my-permissions', async (request, reply) => {
  // Verificar permissões via request
  const canCreatePosts = request.can('create', 'posts')
  const canDeletePosts = request.can('delete', 'posts')
  
  // Avaliação detalhada
  const evaluation = request.evaluate('publish', 'posts')
  
  return {
    permissions: {
      createPosts: canCreatePosts,
      deletePosts: canDeletePosts
    },
    publishEvaluation: evaluation
  }
})

// Verificar via instância Fastify
fastify.get('/check-user/:userId', async (request, reply) => {
  const user = await getUser(request.params.userId)
  const canEdit = fastify.can(user, 'edit', 'profile')
  
  return { canEditProfile: canEdit }
})

Uso com DDD (Domain-Driven Design)

1. Proteger Use Cases

// Use Case
class CreatePostUseCase implements ProtectedUseCase<CreatePostInput, Post> {
  constructor(private postRepository: PostRepository) {}
  
  async execute(input: CreatePostInput, user: User): Promise<Post> {
    const post = new Post({
      ...input,
      authorId: user.id
    })
    
    return this.postRepository.save(post)
  }
}

// Criar guard
const guardedCreatePost = fastify.createUseCaseGuard(
  new CreatePostUseCase(postRepository),
  {
    getPermissions: (input) => ({
      action: 'create',
      resource: 'posts'
    }),
    context: (input, user) => ({
      category: input.category
    })
  }
)

// Usar em rota
fastify.post('/posts', async (request, reply) => {
  const post = await guardedCreatePost(request.body, request.user)
  return { post }
})

2. Decorator para Use Cases

import { RequirePermissions } from '@shield-acl/fastify'

class PostService {
  @RequirePermissions({
    getPermissions: (input) => ({
      action: 'update',
      resource: `posts:${input.id}`
    })
  })
  async updatePost(input: UpdatePostInput, user: User): Promise<Post> {
    // Lógica do use case
  }
  
  @RequirePermissions({
    getPermissions: (input) => [
      { action: 'delete', resource: `posts:${input.id}` },
      { action: 'moderate', resource: 'posts' }
    ]
  })
  async deletePost(input: { id: string }, user: User): Promise<void> {
    // Precisa de ambas as permissões
  }
}

API Completa

Opções do Plugin

interface ShieldACLOptions {
  // Instância do ACL (obrigatório)
  acl: ACL
  
  // Função para extrair usuário da requisição
  getUserFromRequest?: (request: FastifyRequest) => User | null | Promise<User | null>
  
  // Nome da propriedade onde o usuário será armazenado (default: "user")
  userProperty?: string
  
  // Handler customizado para erros de autorização
  errorHandler?: (error: UnauthorizedError, request: FastifyRequest, reply: FastifyReply) => void
  
  // Debug mode (default: false)
  debug?: boolean
}

Decorators do Fastify

// Instância do ACL
fastify.acl: ACL

// Criar middleware de autorização
fastify.authorize(options: AuthorizeOptions): preHandlerHookHandler

// Verificar permissão
fastify.can(user: User, action: string, resource?: string, context?: any): boolean

// Criar guard para use case
fastify.createUseCaseGuard<TInput, TOutput>(
  useCase: ProtectedUseCase<TInput, TOutput>,
  options: UseCaseGuardOptions<TInput>
): (input: TInput, user?: User) => Promise<TOutput>

Decorators da Request

// Usuário atual
request.user: User | undefined

// Verificar permissão
request.can(action: string, resource?: string, context?: any): boolean

// Avaliação detalhada
request.evaluate(action: string, resource?: string, context?: any): EvaluationResult

Opções de Autorização

interface AuthorizeOptions {
  // Ação requerida
  action: string
  
  // Recurso (pode ser função)
  resource?: string | ((request: FastifyRequest) => string)
  
  // Contexto adicional (pode ser função async)
  context?: any | ((request: FastifyRequest) => any | Promise<any>)
  
  // Condição para pular verificação
  skip?: (request: FastifyRequest) => boolean | Promise<boolean>
  
  // Error handler específico
  errorHandler?: (error: UnauthorizedError, request: FastifyRequest, reply: FastifyReply) => void
}

Exemplos Avançados

Autorização Condicional

fastify.get('/posts/:id', {
  preHandler: fastify.authorize({
    action: 'read',
    resource: 'posts',
    skip: async (request) => {
      // Pular autorização se o post é público
      const post = await getPost(request.params.id)
      return post.visibility === 'public'
    }
  })
}, handler)

Contexto Dinâmico

fastify.delete('/posts/:id', {
  preHandler: fastify.authorize({
    action: 'delete',
    resource: 'posts',
    context: async (request) => {
      const post = await getPost(request.params.id)
      return {
        resource: post,
        isOwner: post.authorId === request.user?.id
      }
    }
  })
}, handler)

Error Handler Customizado

await fastify.register(shieldACLPlugin, {
  acl,
  errorHandler: (error, request, reply) => {
    // Log detalhado
    request.log.warn({
      user: request.user?.id,
      action: error.action,
      resource: error.resource,
      reason: error.reason
    }, 'Authorization denied')
    
    // Resposta customizada
    reply.status(403).send({
      error: 'Forbidden',
      message: 'Você não tem permissão para esta ação',
      details: process.env.NODE_ENV === 'development' ? {
        action: error.action,
        resource: error.resource
      } : undefined
    })
  }
})

Integração com Swagger

fastify.get('/admin/users', {
  schema: {
    security: [{ bearerAuth: [] }],
    tags: ['admin'],
    summary: 'List all users',
    response: {
      200: {
        type: 'array',
        items: { $ref: 'User#' }
      },
      403: {
        type: 'object',
        properties: {
          error: { type: 'string' },
          message: { type: 'string' }
        }
      }
    }
  },
  preHandler: fastify.authorize({
    action: 'read',
    resource: 'users'
  })
}, async (request, reply) => {
  return await userService.findAll()
})

Performance

  • Cache automático de avaliações via Shield ACL Core
  • Decorators são criados apenas uma vez durante o registro
  • Verificações síncronas quando possível
  • Suporte a condições assíncronas apenas quando necessário

Testes

# Rodar testes
pnpm test

# Coverage
pnpm test:coverage

# Watch mode
pnpm test:watch

Compatibilidade

  • Fastify 4.x
  • Fastify 5.x
  • Node.js 18+
  • TypeScript 4.5+

Licença

MIT © Anderson D. Rosa