Multi usuários no Laravel
Tags: laravel, php, programming, tutorial
Published: 2023-09-07 14:46:06

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/53yvak5dh0y2k5lbe6ip.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvc70p6g6clr8ofti98e.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xou2bpsj5pv6rdny80kr.png)

### 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');
    }
};
```
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');
        });
    }
};

```
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');
        });
    }
};
```

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;
        }
    }
}
```

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);
    }
}
```

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;
        });
    }
}
```

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;
    });
}
```

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));
}
```

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;
}
```

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0z4s8uiaix8ga7f63u8m.png)

Chegamos ao fim, até mais dev.