Tornando guards configuráveis com @GuardOption
Alguns guards precisam de parâmetros para funcionar. Um guard de cargos precisa saber quais cargos verificar, um guard de cooldown precisa conhecer a janela de tempo e um guard de permissões precisa saber quais permissões exigir. Em vez de fixar esses valores dentro da classe do guard, você declara propriedades configuráveis com o decorator @GuardOption e passa os valores quando anexa o guard.
O decorator @GuardOption aceita um objeto de configuração com dois campos opcionais. O campo required torna a opção obrigatória, o que significa que o framework lança um erro de configuração em runtime se ela não for fornecida. O campo default define um valor de fallback aplicado quando nenhum valor é passado explicitamente. Se nem required nem default forem definidos, a opção será apenas opcional e ficará como undefined quando não for informada.
import { Guard, GuardOption } from '@spraxium/common';
import type { ExecutionContext, SpraxiumGuard } from '@spraxium/common';
@Guard()
export class MinRolesGuard implements SpraxiumGuard {
@GuardOption({ default: 1 })
minRoles!: number;
canActivate(ctx: ExecutionContext): boolean {
const member = ctx.getMember();
if (!member || !('roles' in member)) return false;
return member.roles.cache.size >= this.minRoles;
}
}No exemplo acima, a propriedade minRoles tem valor padrão 1, então, se você anexar esse guard sem especificar um valor, ele exigirá pelo menos um cargo. Se quiser um limite diferente, basta passá-lo por withOptions ao anexar o guard.
O helper withOptions
A função withOptions é a forma de passar opções tipadas para um guard ao anexá-lo com @UseGuards. Ela recebe a classe do guard como primeiro argumento e um objeto de opções como segundo. O sistema de tipos do TypeScript infere o formato dessas opções a partir das propriedades com @GuardOption do guard, então você ganha autocomplete e erros de tipo se passar um formato incorreto.
import { UseGuards, withOptions } from '@spraxium/common';
import { PermissionGuard, RoleGuard } from '@spraxium/core';
import { MinRolesGuard } from '../guards/min-roles.guard';
@UseGuards(
withOptions(RoleGuard, { roles: ['Moderator'] }),
withOptions(PermissionGuard, { permissions: ['BanMembers'] }),
withOptions(MinRolesGuard, { minRoles: 3 }),
)
export class ModerationHandler {
async handle() {
// all three guards must pass before this runs
}
}Se você não precisar passar opções porque o guard não tem opções obrigatórias ou porque os valores padrão já bastam, pode passar a classe do guard diretamente, sem withOptions. Os dois estilos funcionam na mesma chamada de @UseGuards, então você pode misturar guards que precisam de opções com guards que não precisam.
import { UseGuards, withOptions } from '@spraxium/common';
import { GuildOnly, RoleGuard } from '@spraxium/core';
@UseGuards(GuildOnly, withOptions(RoleGuard, { roles: ['Admin'] }))
export class MixedHandler {
async handle() {
// GuildOnly needs no options, RoleGuard needs roles
}
}Guards embutidos
O Spraxium traz dez guards embutidos no pacote core que cobrem os padrões de acesso mais comuns no Discord. Cada guard já vem decorado com @Guard() e pronto para uso com @UseGuards ou useGlobalGuards. A lista abaixo mostra cada guard embutido, o que ele verifica e quais opções espera.
Usando guards embutidos
Guards embutidos sem opções podem ser passados diretamente para @UseGuards ou useGlobalGuards. Guards com opções obrigatórias precisam usar withOptions ou o segundo parâmetro de useGlobalGuards. Aqui estão exemplos práticos com os padrões mais comuns.
import { SpraxiumFactory } from '@spraxium/core';
import { NoBotGuard, OwnerOnly, CooldownGuard } from '@spraxium/core';
const app = await SpraxiumFactory.create({ token: process.env.DISCORD_TOKEN! });
app.useGlobalGuards(NoBotGuard);
app.useGlobalGuards(OwnerOnly, { ownerIds: ['123456789'] });
app.useGlobalGuards(CooldownGuard, { cooldown: 3000 });
await app.listen();O RoleGuard verifica se o membro tem pelo menos um dos cargos listados. Você pode passar nomes de cargos como "Moderator" ou IDs snowflake de cargo como "1234567890". O PermissionGuard verifica se o membro tem todas as permissões listadas, usando strings de permissão do Discord.js como "BanMembers", "ManageGuild" ou "Administrator".
Criando um guard personalizado com opções
Quando os guards embutidos não cobrem seu caso de uso, você cria um guard personalizado que declara propriedades configuráveis por meio de @GuardOption. A classe do guard define quais parâmetros precisa, e cada ponto que a utiliza fornece os valores concretos por meio de withOptions. É assim que guards embutidos como RoleGuard e CooldownGuard funcionam internamente, e você segue exatamente a mesma abordagem nos seus próprios guards.
Defina a classe do guard com @Guard
Comece com uma classe decorada com @Guard() que implemente SpraxiumGuard. O método canActivate contém a lógica de verificação, e qualquer parâmetro configurável recebe o decorator @GuardOption em sua propriedade. Marque opções como required quando o guard não puder funcionar sem elas, ou defina um default quando existir um fallback sensato. O framework lê esses decorators no momento da instanciação e preenche os valores antes mesmo de canActivate ser executado.
Passe opções com withOptions em cada uso
Ao anexar o guard com @UseGuards, envolva-o com withOptions e passe os valores concretos que ele espera. O TypeScript infere o formato das opções a partir das propriedades com @GuardOption, então você recebe autocomplete e erros em tempo de compilação se passar o tipo errado. Se esquecer uma opção required, o framework lança um erro de configuração claro durante o bootstrap.
Reutilize o mesmo guard com valores diferentes
O poder dos guards configuráveis está em uma única classe servir a muitos cenários. Um MinAccountAgeGuard com minDays: 7 em um comando geral e minDays: 90 em um comando de giveaway usa a mesma classe com limites diferentes. Você não precisa criar classes separadas de guard para cada limite.
Aqui está um exemplo completo de um guard personalizado que verifica a idade mínima da conta e suporta várias opções configuráveis. O guard tem uma opção obrigatória e uma opção opcional com valor padrão.
import { Guard, GuardOption } from '@spraxium/common';
import type { ExecutionContext, SpraxiumGuard } from '@spraxium/common';
@Guard()
export class MinAccountAgeGuard implements SpraxiumGuard {
@GuardOption({ required: true })
minDays!: number;
@GuardOption({ default: false })
allowBots!: boolean;
canActivate(ctx: ExecutionContext): boolean {
const user = ctx.getUser();
if (user.bot && !this.allowBots) return false;
const createdAt = user.createdAt.getTime();
const ageInDays = (Date.now() - createdAt) / (1000 * 60 * 60 * 24);
return ageInDays >= this.minDays;
}
}Repare que o guard tem duas opções: minDays é obrigatória porque o guard não consegue funcionar sem saber o limite, e allowBots tem padrão false porque a maioria dos pontos de uso quer bloquear bots. A terceira aba mostra como a mesma classe de guard aceita uma configuração completamente diferente em outro ponto de uso, sem nenhuma duplicação de código.
Comece pelos embutidos
Antes de criar um guard personalizado, verifique se um dos dez guards embutidos já cobre o seu caso de uso. Guards personalizados são poderosos, mas desnecessários quando um embutido já faz exatamente o que você precisa.