thumbnail

Construindo um Sistema de Licença Completo com Laravel e Filament 4.0

Yuri do Monte Yuri do Monte | 11 min de leitura
há 7 meses

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
Este post não possui comentários