Construindo um Sistema de Licença Completo com Laravel e Filament 4.0
O gerenciamento de acesso em aplicações SaaS (Software as a Service) é um componente crítico que impacta diretamente a monetização, a segurança e a escalabilidade do produto. Um sistema de licenciamento bem arquitetado não apenas garante que o acesso seja concedido somente a usuários autorizados, mas também automatiza o controle de validade dessas licenças, um pilar para qualquer modelo de negócio baseado em assinaturas.
Neste artigo técnico, faremos um mergulho profundo na construção de um sistema de licenças robusto e automatizado, utilizando o framework Laravel e o painel de administração Filament 4.0. Abordaremos desde a modelagem do banco de dados e a criação de uma interface de gerenciamento intuitiva até a implementação de rotinas automatizadas para desativar licenças e usuários expirados, garantindo um controle de acesso seguro e de fácil manutenção.
Arquitetura do Banco de Dados: Os Models Essenciais
Uma fundação sólida começa com uma modelagem de dados clara e coesa. Para nosso sistema, a arquitetura gira em torno de três models principais: Enterprise, License e User. Juntos, eles formam a espinha dorsal do nosso controle de acesso e da lógica de multi-tenancy.
O Model Enterprise
Em um ambiente multi-tenant, o model Enterprise representa cada cliente ou organização que utiliza o sistema. Ele funciona como o "inquilino" (tenant), permitindo o isolamento de dados entre as diferentes contas.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Enterprise extends Model
{
use SoftDeletes;
protected $fillable = [
'name',
'slug',
'logo',
'description',
];
public function users(): HasMany
{
return $this->hasMany(User::class);
}
public function clients(): HasMany
{
return $this->hasMany(Client::class);
}
}
Análise do Código:
- use SoftDeletes;: Implementa a exclusão lógica, permitindo que registros de empresas sejam "desativados" sem removê-los permanentemente do banco de dados.
- $fillable: Define os campos que podem ser preenchidos em massa, protegendo contra vulnerabilidades de mass assignment.
- users(): HasMany: Estabelece a relação de que uma Enterprise pode ter múltiplos User, fundamental para a organização dos inquilinos.
O Model License
Este é o model central do nosso sistema. Ele armazena a chave de licença, sua data de expiração e o status (ativa ou inativa). A lógica para validar uma licença é encapsulada aqui, mantendo o código limpo e reutilizável.
<?php
namespace App\Models;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class License extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'enterprise_id',
'key',
'is_active',
'expires_at',
];
protected $casts = [
'expires_at' => 'datetime',
'is_active' => 'boolean',
];
public static function generateKey(): string
{
return strtoupper(Str::random(60));
}
public function isValid(): bool
{
return $this->is_active &&
(is_null($this->expires_at) || $this->expires_at->isFuture());
}
public function users()
{
return $this->hasMany(User::class);
}
public function enterprise()
{
return $this->belongsTo(Enterprise::class);
}
}
Análise do Código:
- $casts: Converte automaticamente os campos expires_at para um objeto Carbon e is_active para um booleano, facilitando a manipulação desses dados.
- generateKey(): string: Um método estático utilitário para gerar uma chave de licença única e aleatória com 60 caracteres maiúsculos.
- isValid(): bool: O método mais importante deste model. Ele centraliza a regra de negócio para a validação de uma licença, verificando duas condições: se a licença está marcada como is_active e se a data em expires_at ainda não passou (ou é nula).
- enterprise(): Define a relação de que uma License pertence a uma Enterprise.
O Model User
O model User integra-se diretamente com o sistema de autenticação do Laravel e com o Filament para controlar o acesso. É aqui que a validade da licença é verificada no momento do login.
<?php
namespace App\Models;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Filament\Models\Contracts\FilamentUser;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class User extends Authenticatable implements FilamentUser
{
// ... fillable, hidden, casts ...
public function enterprise(): BelongsTo
{
return $this->belongsTo(Enterprise::class);
}
public function license(): BelongsTo
{
return $this->belongsTo(License::class);
}
public function hasValidLicense(): bool
{
$license = $this->license; // Carrega a licença associada
return $license?->isValid() ?? false;
}
public function isSuperAdmin(): bool
{
return $this->is_admin === true;
}
public function canAccessPanel(Panel $panel): bool
{
if ($this->isSuperAdmin()) {
return true;
}
return $this->is_active && $this->hasValidLicense();
}
}
Análise do Código:
implements FilamentUser: Contrato do Filament que identifica este model como um usuário do painel.
license(): BelongsTo: Estabelece que um User está diretamente associado a uma License.
hasValidLicense(): bool: Este método verifica se o usuário possui uma licença associada e, em caso afirmativo, delega a lógica de validação para o método isValid() do model License. O operador null-safe (?->) previne erros caso o usuário não tenha uma licença.
canAccessPanel(Panel $panel): bool: Este é o ponto de controle de acesso do Filament. Ele é executado toda vez que um usuário tenta acessar o painel. A lógica é clara: permite o acesso se o usuário for um superadministrador (isSuperAdmin) ou se ele estiver ativo (is_active) E possuir uma licença válida (hasValidLicense).
Interface de Gerenciamento com Filament
O Filament simplifica drasticamente a criação de interfaces de administração. Usando Schemas, podemos definir formulários complexos de forma declarativa.
Formulário de Enterprise
<?php
namespace App\Filament\System\Resources\Enterprises\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;
class EnterpriseForm
{
public static function configure(Schema $schema): Schema
{
return $schema->components([
TextInput::make('name')
->label('Nome')
->required()
->live()
->afterStateUpdated(fn ($state, $set) => $set('slug', Str::slug($state))),
TextInput::make('slug')
->required()
->unique(ignoreRecord: true)
->disabled()
->dehydrated(),
// ... outros campos
]);
}
}
Análise do Código:
- live() e afterStateUpdated(): Uma funcionalidade poderosa do Filament. O campo name atualiza o slug em tempo real conforme o usuário digita, criando uma experiência de usuário fluida.
- disabled() e dehydrated(): O campo slug é desabilitado para edição direta pelo usuário, mas seu valor é salvo (dehydrated) no banco de dados.
Formulário de License
<?php
namespace App\Filament\System\Resources\Licenses\Schemas;
use App\Models\License;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
class LicenseForm
{
public static function configure(Schema $schema): Schema
{
return $schema->components([
Select::make('enterprise_id')
->relationship('enterprise', 'name')
->required(),
TextInput::make('key')
->default(fn() => License::generateKey())
->required(),
DatePicker::make('expires_at')
->label('Data de Expiração'),
Toggle::make('is_active')
->label('Ativo')
->default(true),
]);
}
}
Análise do Código:
- Select::make('enterprise_id')->relationship(...): Cria um dropdown populado com os nomes das empresas, facilitando a associação da licença.
- TextInput::make('key')->default(...): Ao criar uma nova licença, o campo key já vem preenchido com uma chave gerada pelo nosso método License::generateKey().
- DatePicker e Toggle: Componentes visuais que simplificam a seleção de datas e a ativação/desativação da licença.
Automação: Desativação de Licenças e Usuários
Manter a integridade do sistema exige que licenças e usuários expirados sejam desativados automaticamente. O Laravel oferece os Jobs para encapsular e enfileirar essas tarefas.
Job DisableExpiredLicense
Este Job é responsável por encontrar e desativar todas as licenças que já expiraram.
<?php
namespace App\Jobs;
use App\Models\License;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class DisableExpiredLicense implements ShouldQueue
{
use Queueable;
public function handle(): void
{
$expired_licenses = License::where('is_active', true)
->where('expires_at', '<', now())
->get();
if ($expired_licenses->count() > 0) {
foreach ($expired_licenses as $license) {
$license->is_active = false;
$license->save();
\Log::info("Licença " . $license->key . " foi desativada.");
}
}
}
}
Análise do Código:
- implements ShouldQueue: Indica ao Laravel que este Job deve ser executado de forma assíncrona por um worker, não bloqueando a aplicação principal.
- handle(): void: Contém a lógica principal. A query busca licenças ativas cuja data de expiração é anterior ao momento atual. Em seguida, itera sobre os resultados e define is_active como false.
Job DisableExpiredUsers
De forma similar, este Job desativa usuários cuja data de acesso (valid_until) expirou.
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class DisableExpiredUsers implements ShouldQueue
{
use Queueable;
public function handle(): void
{
$expired_users = User::where('is_active', true)
->where('valid_until', '<', now())
->get();
if ($expired_users->count() > 0) {
foreach ($expired_users as $user) {
$user->is_active = false;
$user->save();
\Log::info("Usuário " . $user->name . " foi desativado.");
}
}
}
}
Para automatizar a execução desses Jobs, basta agendá-los no app/Console/Kernel.php:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
$schedule->job(new \App\Jobs\DisableExpiredLicense)->daily();
$schedule->job(new \App\Jobs\DisableExpiredUsers)->daily();
}
Conclusão
Ao combinar a modelagem expressiva do Eloquent, a interface de administração ágil do Filament 4.0 e o poder de automação dos Jobs do Laravel, construímos um sistema de licenciamento completo, seguro e escalável. A lógica de acesso centralizada no método canAccessPanel garante que as regras de negócio sejam aplicadas de forma consistente, enquanto os Jobs mantêm a integridade dos dados sem qualquer intervenção manual.
Esta arquitetura não apenas protege a receita do seu SaaS, mas também fornece uma base sólida para futuras expansões, como a implementação de diferentes planos de assinatura, recursos por nível de licença e um portal de autogerenciamento para os clientes. Com essas ferramentas, você tem o controle total sobre o ciclo de vida das licenças em sua aplicação.
Comentários
Realize login para comentar neste post