DEV Community

Cover image for Multi usuários no Laravel
Jhonatan Henkel
Jhonatan Henkel

Posted on

Multi usuários no Laravel

Olá dev, hoje o assunto vai ser um pouco mais longo, vamos conversar um pouco sobre multi usuários no Laravel, essa arquitetura é chamada de multi tenant, ou multi tenancy.

Tenant vem de hóspede, pois cada usuário é equivalente a um hóspede nessa arquitetura.

Tipos de multi tenant

Segundo meus estudos, existem três principais formas para aplicar essa arquitetura em seu projeto:

Por mais dos diversos lugares que eu li e vi, geralmente se resumia nessas três abordagens.

1-Com banco de dados separado: em alguns casos, podemos ter até mesmo uma instância inteira da aplicação por cliente. Isso seria o mais seguro, se considerar o isolamento dos dados, porém acaba que a manutenção, o gerenciamento e o custo dessa abordagem tornam-se caras e difíceis.

Com banco de dados separado

2-Uma única instância de banco com esquemas separados por cliente: nessa abordagem para o meu uso, na minha aplicação (vou detalhar mais a frente), não parecia ser muito viável, pois para cada novo usuário, eu teria que fazer um create database tenantX.

_Uma única instância de banco com esquemas separados por cliente

3-Um único banco de dados para todos os tenants: essa foi a abordagem que escolhi. Nessa modalidade, é feito o vinculo dos usuários por um id, uma "chave de locação". Aqui, assim como nas outras abordagens, um usuário não vê os dados dos outros usuários, obviamente. A não ser que você defina um mesmo tenant_id em outro usuário.

Um único banco de dados para todos os tenants

Sobre minha aplicação

Antes de irmos para o código, vamos bater mais um papo, sobre a minha aplicação.

No momento em que escrevo, minha aplicação está desenvolvida com o backend em Laravel e o frontend em Vue.Js. A comunicação do Laravel com o Vue está totalmente sendo feita por API, ou seja, o backend está bem separado do frontend.

Para fazer a autenticação (Login) na aplicação, estou usando JTW, no momento que o response do login é feito para o frontend, o token e usuário é salvo no gerenciador de estado do Vue, o Pinia.

Isso garante que caso o usuário troque os dados no localStorage do navegador, não afete a aplicação, acessando dados que não deve.

Ainda não terei tela de registro, por questões de que ainda não quero liberar a aplicação para todos usarem, então os usuários vão ser inseridos manualmente no banco.

Sem mais enrolação, vamos ao que interessa…

Preparando o Banco

Neste passo vamos ter que criar três migrations:

1-Criando a tabela de tenants:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tenants', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id')->nullable()->index();
            $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
        });
    }
    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tenants');
    }
};
Enter fullscreen mode Exit fullscreen mode

2-Alterar a tabela de usuário para ter o tenant_id:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function(Blueprint $table) {
            $table->unsignedBigInteger('tenant_id')->nullable()->after('name')->index();
            $table->foreign('tenant_id')->references('id')->on('tenants');
        });
    }
    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function($table) {
            $table->dropColumn('tenant_id');
        });
    }
};

Enter fullscreen mode Exit fullscreen mode

3-Alterar demais tabelas para ter o tenant_id:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('tabela_1', function(Blueprint $table) {
            $table->unsignedBigInteger('tenant_id')->nullable()->after('description')->index();
            $table->foreign('tenant_id')->references('id')->on('tenants');
        });
        Schema::table('tabela_2', function(Blueprint $table) {
            $table->unsignedBigInteger('tenant_id')->nullable()->after('next_installment')->index();
            $table->foreign('tenant_id')->references('id')->on('tenants');
        });
    }
    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('tabela_1', function(Blueprint $table) {
            $table->dropColumn('tenant_id');
        });
        Schema::table('tabela_2', function(Blueprint $table) {
            $table->dropColumn('tenant_id');
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

Da parte de preparar o banco é isso, vamos ao próximo passo, que é na aplicação

Classe JWT

Antes de iniciarmos com o código do multi-tenant, vamos dar uma olhada na minha classe do JWT.

<?php

namespace App\Tools\Auth;

use App\Enums\DateEnum;
use App\Models\User;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JwtTools
{
    public static function createJWT(User $data): string
    {
        $payload = array(
            'exp' => time() + DateEnum::TREE_HOUR_IN_SECONDS,
            'iat' => time(),
            'data' => $data
        );
        return JWT::encode($payload, env('SECRET_HASH'), 'HS256');
    }

    public static function validateJWT(string $authorization): bool|object
    {
        try {
            $token = str_replace('Bearer ', '', $authorization);
            $key = new Key(env('SECRET_HASH'), 'HS256');
            return JWT::decode($token, $key);
        } catch (\Exception $e) {
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

No createJWT, basicamente estou gerando o token com o model User, com isso, quando eu receber o token do frontend novamente e descriptografar, já tenho o model do User, sem a necessidade de ficar fazendo find no banco.

Já no validateJWT, vamos pegar esse token gerado anteriormente e descriptografar, podendo retornar o objeto JWT com o model User, ou false, caso não consiga descriptografar.

Classe TenantScope

Nessa classe, é onde vamos adicionar ao scope do Laravel a busca pelo parâmetro tenant_id de cada tabela, sem termos a necessidade de ficar fazendo em cada consulta ao banco.

A responsabilidade dela é adicionar a toda consulta a condição where filtrando pelo tenant_id no repository pesquisado, evitando assim esquecimentos.

<?php
namespace App\Scope;
use App\Enums\ConfigEnum;
use App\Tools\Auth\JwtTools;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $token = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? '';
        $user = JwtTools::validateJWT($token);
        if (! $user) {
            return;
        }
        $builder->where($model->getTable() . '.tenant_id', $user->data->tenant_id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, basicamente pego o token da request, aquele feito pelo JWT mencionado anteriormente, caso consiga descriptografar ele, teremos o objeto do user, tendo assim o seu tenant_id. Caso não tiver, só dá um return, evitando quebras.

Calma, a obrigação do envio do token na request é responsabilidade de outra classe, vamos ver mais a frente.

Trait Tenantable

Essa é a trait que vai ser responsável por injetar o tenant_id no nosso model.

<?php

namespace App\Models\Trait;

use App\Scope\TenantScope;
use App\Models\Tenant;
use App\Tools\Auth\JwtTools;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait Tenantable
{
    protected static function bootTenantable(): void
    {
        static::addGlobalScope(new TenantScope);
        $token = $_SERVER['HTTP_X_MFP_USER_TOKEN'] ?? '';
        $user = JwtTools::validateJWT($token);
        if (! $user) {
            return;
        }
        static::creating(function ($model) use ($user) {
            $model->tenant_id = $user->data->tenant_id;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Basicamente a condicional é a mesma da classe anterior, a diferença é que que estamos definindo o tenant_id do model que usar essa trait.

Com isso, sempre que formos fazer um insert por exemplo, sempre teremos definido no model o tenant_id, sem a necessidade de popular manualmente esse atributo.

Com essa trait criada, vamos adicionar ela a todo model que deva fazer o uso do tenant_id. Basta adicionar o "use Tenantable;" em cada model.

Validando requests API e model Tenant

Na classe AuthServiceProvider, iremos fazer uma alteração no método boot. Aqui é onde iremos validar e obrigar o envio do token API e do token JWT.

public function boot(): void
{
    Auth::viaRequest('authApi', function (Request $request) {
        $userToken = $_SERVER[ConfigEnum::X_USER_TOKEN] ?? '';
        $user = JwtTools::validateJWT($userToken);
        $apiTokenEncrypted = bcrypt(env('SECRET_HASH'));
        $apiToken = $request->header('API_TOKEN') ?? '';
        return password_verify($apiToken, $apiTokenEncrypted) && $user ? new User() : null;
    });
}
Enter fullscreen mode Exit fullscreen mode

Caso esse método retorne o null, vai ser respondido o conteúdo definido dentro do método unauthenticated dentro da classe AuthenticateAPI. A minha eu deixei assim:

protected function unauthenticated($request, array $guards): ?string
{
    $message = 'Tokens obrigatórios ausentes ou inválidos!';
    abort(response()->json($message, Response::HTTP_UNAUTHORIZED));
}
Enter fullscreen mode Exit fullscreen mode

Também temos que criar um model de Tenant. Não precisa de nada especial dentro dele, só ter ele criado mesmo.

namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
    use HasFactory;
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Com isso tudo feito, já deve funcionar multi tenant na sua aplicação. Isso que eu mostrei aqui é algo relativamente super simples, porém senti que falta conteúdo falando sobre. Até achei um vídeo (aqui), porém não funcionou com o Vue devido a comunicação ser por API. Para funcionar na nessa modalidade tive que fazer as alterações mencionadas aqui.

No fim ficou algo mais ou menos assim:

conclusao

Chegamos ao fim, até mais dev.

Top comments (0)