1.1.0 • Published 1 month ago
@shield-acl/fastify v1.1.0
@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
1.1.0
1 month ago