diff --git a/app/Http/Controllers/AlunosController.php b/app/Http/Controllers/AlunosController.php index 2acbb1b..53640a6 100644 --- a/app/Http/Controllers/AlunosController.php +++ b/app/Http/Controllers/AlunosController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Models\alunos; +use App\Models\Aluno; use App\Models\turmas; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -11,20 +11,45 @@ class AlunosController extends Controller { public function index() { - $escolaId = Auth::user()->id_escola; - if ($escolaId == 0) { - $alunos = alunos::all(); - } else { - $alunos = []; - $turmas = turmas::where('id_escola', $escolaId)->get(); - foreach ($turmas as $turma) { - $alunosget = alunos::where('id_turma', $turma->id)->get(); + $user = Auth()->user(); - foreach ($alunosget as $alunosg) { - $alunos[] = $alunosg; - } - } + // coordenação/secretaria (ou quem tiver) + if ($user->can('alunos.ver_todos')) { + $alunos = Aluno::query()->get(); + return view('escolas.alunos', compact('alunos')); } - return view('escolas.alunos', compact('alunos')); + + // aluno/responsável + if ($user->can('alunos.ver_meus')) { + $alunos = Aluno::query() + ->where(function ($q) use ($user) { + $q->where('user_id', $user->id) + ->orWhereHas('responsaveis', fn($r) => $r->where('users.id', $user->id)); + }) + ->get(); + + return view('alunos.index', compact('alunos')); + } + + abort(403); + } + + public function updateOrCreate(Request $request) + { + + $aluno = Aluno::updateOrCreate( + ['cpf' => $request->cpf], + [ + 'nome' => $request->nome, + 'id_turma' => $request->id_turma, + 'id_escola' => $request->id_escola, + 'data_nascimento' => $request->data_nascimento, + 'data_inscricao' => $request->data_inscricao, + 'cpf' => $request->cpf, + 'user_id' => $request->user_id, + 'responsavel_user_id' => $request->responsavel_user_id, + ] + ); + return redirect()->route('alunos')->with('success', 'Aluno Criado|Atualizado com sucesso.'); } } diff --git a/app/Http/Controllers/ConfigsController.php b/app/Http/Controllers/ConfigsController.php new file mode 100644 index 0000000..82148f0 --- /dev/null +++ b/app/Http/Controllers/ConfigsController.php @@ -0,0 +1,10 @@ +id_escola == 0) { - $escolas = escolas::all(); - } else { - $escolas = escolas::where('id', Auth::user()->id_escola)->get(); - } + $escolas = Escola::all(); return view('escolas.index', compact('escolas')); } + public function createOrUpdate(Request $request) - { - try { - escolas::updateOrCreate([ - 'cnpj' => $request->cnpj, - ],[ +{ + DB::beginTransaction(); + + try { + + $escola = Escola::updateOrCreate( + ['cnpj' => $request->cnpj], + [ 'nome' => $request->razaosocial, 'endereco' => json_decode($request->address, true), - ]); - return redirect()->route('escolas')->with('success', 'Escola Criada|Atualizada com sucesso'); - } catch (\Throwable $th) { - dd($th); + ] + ); + + if ($escola->wasRecentlyCreated) { + + // 🔥 Limpa cache ANTES + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + // Define tenant (team) + setPermissionsTeamId($escola->id); + + /* + |-------------------------------------------------------------------------- + | ROLES (POR ESCOLA) + |-------------------------------------------------------------------------- + */ + $roles = [ + 'coordenacao', + 'secretaria', + 'responsavel', + 'aluno', + 'representante_de_turma', + ]; + + foreach ($roles as $r) { + Role::firstOrCreate([ + 'name' => $r, + 'guard_name' => 'web', + 'id_escola' => $escola->id, + ]); + } + + /* + |-------------------------------------------------------------------------- + | PERMISSIONS + |-------------------------------------------------------------------------- + */ + $permissions = [ + // escolas + 'escolas.ver', + 'escolas.criar', + 'escolas.editar', + 'escolas.deletar', + + // turmas + 'turmas.ver', + 'turmas.detalhes', + 'turmas.criar', + 'turmas.editar', + 'turmas.deletar', + + // alunos + 'alunos.ver_todos', + 'alunos.ver_meus', + 'alunos.criar', + 'alunos.editar', + 'alunos.deletar', + + // whatsapp + 'wpp.enviar', + ]; + + foreach ($permissions as $p) { + Permission::firstOrCreate([ + 'name' => $p, + 'guard_name' => 'web', + ]); + } + + /* + |-------------------------------------------------------------------------- + | MAPEAR ROLE -> PERMISSIONS (CORRIGIDO) + |-------------------------------------------------------------------------- + */ + $rolePermissions = [ + + // Coordenação: vê todos os alunos + 'coordenacao' => [ + 'turmas.ver', + 'turmas.detalhes', + 'turmas.criar', + 'turmas.editar', + 'turmas.deletar', + + 'alunos.ver_todos', + 'alunos.criar', + 'alunos.editar', + 'alunos.deletar', + + 'wpp.enviar', + ], + + // Secretaria: vê todos os alunos + 'secretaria' => [ + 'turmas.ver', + 'turmas.detalhes', + + 'alunos.ver_todos', + 'alunos.criar', + 'alunos.editar', + + 'wpp.enviar', + ], + + // Responsável: vê somente os "meus" + 'responsavel' => [ + 'alunos.ver_meus', + ], + + // Aluno: vê somente os "meus" + 'aluno' => [ + 'alunos.ver_meus', + ], + + // Representante: vê os "meus" + detalhes da turma (e wpp se você quiser) + 'representante_de_turma' => [ + 'alunos.ver_meus', + 'turmas.detalhes', + 'wpp.enviar', + ], + ]; + + foreach ($rolePermissions as $roleName => $perms) { + + $role = Role::query() + ->where('name', $roleName) + ->where('guard_name', 'web') + ->where('id_escola', $escola->id) + ->firstOrFail(); + + $role->syncPermissions($perms); + } + + // 🔥 Limpa cache DEPOIS + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + // opcional: volta contexto global + setPermissionsTeamId(null); } + + DB::commit(); + + return redirect() + ->route('escolas') + ->with('success', 'Escola Criada|Atualizada com sucesso'); + + } catch (\Throwable $th) { + DB::rollBack(); + throw $th; } } + + +} diff --git a/app/Http/Controllers/TurmasController.php b/app/Http/Controllers/TurmasController.php index b0fedc4..2b936a3 100644 --- a/app/Http/Controllers/TurmasController.php +++ b/app/Http/Controllers/TurmasController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers; -use App\Models\turmas; +use App\Models\Turma; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -11,21 +11,17 @@ class TurmasController extends Controller public function index() { - if (Auth::user()->id_escola == 0) { - $turmas = turmas::all(); - } else { - $turmas = turmas::where('id_escola', Auth::user()->id_escola)->get(); - } + $turmas = Turma::all(); return view('escolas.turmas', compact('turmas')); } public function createOrUpdate(Request $request) { - $turma = turmas::where('nome', '=', $request->turma_nome)->count(); + $turma = Turma::where('nome', '=', $request->turma_nome)->count(); if ($turma == 0) { try { - $turma = new turmas(); + $turma = new Turma(); $turma->id_escola = $request->id_escola; $turma->nome = $request->turma_nome; $turma->descricao = $request->turma_descricao; @@ -37,7 +33,7 @@ class TurmasController extends Controller } } else { try { - turmas::where('nome', $request->turma_nome)->update([ + Turma::where('nome', $request->turma_nome)->update([ 'nome' => $request->turma_nome, 'descricao' => $request->turma_descricao, 'id_escola' => $request->id_escola, @@ -50,8 +46,31 @@ class TurmasController extends Controller } } - public function detalhes ($id) { - $turma = turmas::findOrFail($id); - return view('escolas.turmas_detalhes', compact('turma')); + public function detalhes($id) + { + $turma = Turma::findOrFail($id); + return view('escolas.Turma_detalhes', compact('turma')); + } + + public function byEscola(Request $request, $escolaId) + { + // Se você tem multi-tenant e o usuário NÃO é super admin, + // é boa prática garantir que ele só consulte escola permitida. + $user = auth()->user(); + + if (!$user->is_super_admin) { + // se você usa pivot user_escolas: + $allowed = $user->escolas()->where('escolas.id', $escolaId)->exists(); + if (!$allowed) { + abort(403); + } + } + + $turmas = Turma::query() + ->where('id_escola', $escolaId) + ->orderBy('nome') + ->get(['id', 'nome']); + + return response()->json($turmas); } } diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php new file mode 100644 index 0000000..ba0d2dc --- /dev/null +++ b/app/Http/Controllers/UsersController.php @@ -0,0 +1,157 @@ +user(); + + if (!$user->is_super_admin) { + $allowed = $user->escolas()->where('escolas.id', $escolaId)->exists(); + abort_if(!$allowed, 403); + } + + // roles por escola + $roles = Role::query() + ->where('id_escola', $escolaId) + ->orderBy('name') + ->get(['id', 'name']); + + return response()->json($roles); + } + + public function alunosByEscola(Request $request, $escolaId) + { + $user = auth()->user(); + + if (!$user->is_super_admin) { + $allowed = $user->escolas()->where('escolas.id', $escolaId)->exists(); + abort_if(!$allowed, 403); + } + + // alunos por escola (sem global scope, se necessário) + $alunos = Aluno::query() + ->withoutGlobalScope('escola') // se seu Aluno usa BelongsToEscola, isso evita filtrar errado + ->where('id_escola', $escolaId) + ->orderBy('nome') + ->get(['id', 'nome']); + + return response()->json($alunos); + } + + public function store(Request $request) + { + // validação base + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:6', 'confirmed'], + + 'escola_id' => ['nullable', 'integer', 'exists:escolas,id'], + 'role_name' => ['nullable', 'string'], // coordenacao/secretaria/responsavel/aluno/representante_de_turma + 'is_super_admin' => ['nullable', 'boolean'], + + // usado quando role for aluno/responsavel + 'aluno_id' => ['nullable', 'integer', 'exists:alunos,id'], + ]); + + // super admin não precisa escola/role + $isSuper = (bool)($data['is_super_admin'] ?? false); + if ($isSuper) { + $data['escola_id'] = null; + $data['role_name'] = null; + $data['aluno_id'] = null; + } else { + // usuário comum precisa escola + role + if (empty($data['escola_id']) || empty($data['role_name'])) { + return back()->withErrors(['escola_id' => 'Selecione escola e cargo.'])->withInput(); + } + } + + // Se for responsável/aluno, aluno_id é obrigatório + if (!$isSuper && in_array($data['role_name'], ['responsavel', 'aluno'], true)) { + if (empty($data['aluno_id'])) { + return back()->withErrors(['aluno_id' => 'Selecione o aluno para vincular.'])->withInput(); + } + + // valida se o aluno pertence à escola selecionada + $ok = Aluno::query() + ->withoutGlobalScope('escola') + ->where('id', $data['aluno_id']) + ->where('id_escola', $data['escola_id']) + ->exists(); + + if (!$ok) { + return back()->withErrors(['aluno_id' => 'Aluno não pertence à escola selecionada.'])->withInput(); + } + } + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + // cria usuário + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'is_super_admin' => $isSuper, + 'current_escola_id' => $data['escola_id'] ?? null, + ]); + + // vincula à escola (pivot) + if (!$isSuper && $user->current_escola_id) { + $user->escolas()->syncWithoutDetaching([$user->current_escola_id]); + + // seta team e atribui role da escola + setPermissionsTeamId($user->current_escola_id); + + $role = Role::query() + ->where('name', $data['role_name']) + ->where('guard_name', 'web') + ->where('id_escola', $user->current_escola_id) + ->firstOrFail(); + + $user->syncRoles([$role]); + + // associações automáticas com aluno + if ($data['role_name'] === 'responsavel') { + Aluno::query() + ->withoutGlobalScope('escola') + ->where('id', $data['aluno_id']) + ->update(['responsavel_user_id' => $user->id]); + } + + if ($data['role_name'] === 'aluno') { + Aluno::query() + ->withoutGlobalScope('escola') + ->where('id', $data['aluno_id']) + ->update(['user_id' => $user->id]); + } + } + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + setPermissionsTeamId(null); + + return back()->with('success', 'Usuário criado com sucesso!'); + } +} diff --git a/app/Http/Middleware/SetTenantMiddleware.php b/app/Http/Middleware/SetTenantMiddleware.php new file mode 100644 index 0000000..5985c54 --- /dev/null +++ b/app/Http/Middleware/SetTenantMiddleware.php @@ -0,0 +1,20 @@ +check()) { + setPermissionsTeamId(auth()->user()->current_escola_id); + } else { + setPermissionsTeamId(null); + } + + return $next($request); + } +} diff --git a/app/Jobs/Envio_Mensagem_Whatsapp.php b/app/Jobs/Envio_Mensagem_Whatsapp.php index 234a826..053730e 100644 --- a/app/Jobs/Envio_Mensagem_Whatsapp.php +++ b/app/Jobs/Envio_Mensagem_Whatsapp.php @@ -1,8 +1,9 @@ idTurma); + $turma = Turma::findOrFail($this->idTurma); + $token = configs::where('nome', '=', 'wuzapi_token')->first(); $detalhes = []; @@ -99,7 +101,7 @@ class Envio_Mensagem_Whatsapp implements ShouldQueue case 'texto': $response = Http::withHeaders([ 'accept' => 'application/json', - 'token' => '3aCSVE4jS9h233mNBr1awz3DF0In8CFk', + 'token' => $token->valor, ]) ->post('https://waha.cae.app.br/chat/send/text', [ 'phone' => $turma->id_whatsapp, @@ -177,7 +179,7 @@ class Envio_Mensagem_Whatsapp implements ShouldQueue case 'imagem'; $response = Http::withHeaders([ 'accept' => 'application/json', - 'token' => '3aCSVE4jS9h233mNBr1awz3DF0In8CFk', + 'token' => $token->valor, ]) ->post('https://waha.cae.app.br/chat/send/image', [ 'phone' => $turma->id_whatsapp, diff --git a/app/Models/Aluno.php b/app/Models/Aluno.php new file mode 100644 index 0000000..95738c8 --- /dev/null +++ b/app/Models/Aluno.php @@ -0,0 +1,62 @@ +belongsTo( + Turma::class, + 'id_turma' + ); + } + + public function escola() + { + return $this->belongsTo( + Escola::class, + 'id_escola' + ); + } + + /** + * Usuário do aluno (se o aluno tiver login no portal) + */ + public function user() + { + return $this->belongsTo( + User::class, + 'user_id' + ); + } + + /** + * Responsáveis do aluno (N:N via pivot aluno_responsaveis) + */ + public function responsaveis() + { + return $this->belongsToMany( + User::class, + 'aluno_responsaveis', + 'aluno_id', + 'user_id' + )->withTimestamps(); + } +} diff --git a/app/Models/Escola.php b/app/Models/Escola.php new file mode 100644 index 0000000..9d7c1e1 --- /dev/null +++ b/app/Models/Escola.php @@ -0,0 +1,66 @@ + 'array', + ]; + + protected $fillable = [ + 'cnpj', + 'endereco', + 'nome', + ]; + + /* + |-------------------------------------------------------------------------- + | GLOBAL SCOPE MULTI-TENANT + |-------------------------------------------------------------------------- + */ + + protected static function booted() + { + static::addGlobalScope('user_escolas', function (Builder $builder) { + + if (Auth::check() && !Auth::user()->is_super_admin) { + + $builder->whereIn('id', function ($query) { + + $query->select('escola_id') + ->from('user_escolas') + ->where('user_id', Auth::id()); + + }); + + } + + }); + } + + /* + |-------------------------------------------------------------------------- + | RELACIONAMENTOS + |-------------------------------------------------------------------------- + */ + + public function turmas() + { + return $this->hasMany(Turma::class, 'id_escola'); + } + + public function users() + { + return $this->belongsToMany( + User::class, + 'user_escolas', + 'escola_id', + 'user_id' + ); + } +} diff --git a/app/Models/Traits/BelongsToEscola.php b/app/Models/Traits/BelongsToEscola.php new file mode 100644 index 0000000..bb3c3dd --- /dev/null +++ b/app/Models/Traits/BelongsToEscola.php @@ -0,0 +1,50 @@ +user(); + + // Sem usuário logado: não filtra + if (!$user) { + return; + } + + // Super admin: vê tudo (não filtra por escola) + if ($user->is_super_admin) { + return; + } + + // Usuário comum: filtra pela escola atual + if ($user->current_escola_id) { + $builder->where('id_escola', $user->current_escola_id); + } + }); + + static::creating(function ($model) { + + $user = auth()->user(); + + if (!$user) { + return; + } + + // Super admin: não força id_escola automaticamente + if ($user->is_super_admin) { + return; + } + + // Se não veio id_escola, seta pela escola atual do usuário + if (!$model->id_escola) { + $model->id_escola = $user->current_escola_id; + } + }); + } +} diff --git a/app/Models/Turma.php b/app/Models/Turma.php new file mode 100644 index 0000000..7266adb --- /dev/null +++ b/app/Models/Turma.php @@ -0,0 +1,24 @@ +belongsTo( + Escola::class, + 'id_escola' + ); + } + +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..ac7aac9 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,47 +2,131 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; +use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasRoles; - /** - * The attributes that are mass assignable. - * - * @var list - */ protected $fillable = [ 'name', 'email', 'password', + 'is_super_admin', + 'current_escola_id', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ protected $hidden = [ 'password', 'remember_token', ]; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array + /* + |-------------------------------------------------------------------------- + | RELACIONAMENTOS + |-------------------------------------------------------------------------- + */ + + public function escolas() { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', + return $this->belongsToMany( + Escola::class, + 'user_escolas', + 'user_id', + 'escola_id' + ); + } + + public function escolaAtual() + { + return $this->belongsTo( + Escola::class, + 'current_escola_id' + ); + } + + /** + * Alunos pelos quais este usuário é responsável (N:N via pivot) + */ + public function alunosComoResponsavel() + { + return $this->belongsToMany( + Aluno::class, + 'aluno_responsaveis', + 'user_id', + 'aluno_id' + )->withTimestamps(); + } + + /** + * Se você quiser acessar o "aluno" do próprio usuário (portal do aluno), + * dá pra ter 1:1 (um usuário pode ser o aluno). + */ + public function aluno() + { + return $this->hasOne(Aluno::class, 'user_id'); + } + + /* + |-------------------------------------------------------------------------- + | SUPER ADMIN + |-------------------------------------------------------------------------- + */ + + public function isSuperAdmin(): bool + { + return (bool) $this->is_super_admin; + } + + /* + |-------------------------------------------------------------------------- + | FUNÇÃO / CARGO DO USUÁRIO (ROLE) + |-------------------------------------------------------------------------- + */ + + public function funcao(): ?string + { + if ($this->isSuperAdmin()) { + return 'Super Admin'; + } + + if (!$this->current_escola_id) { + return null; + } + + setPermissionsTeamId($this->current_escola_id); + + $role = $this->roles()->value('name'); + + if (!$role) { + return null; + } + + $map = [ + 'coordenacao' => 'Coordenação', + 'secretaria' => 'Secretaria', + 'responsavel' => 'Responsável', + 'aluno' => 'Aluno', + 'representante_de_turma' => 'Representante de Turma', + 'super_admin' => 'Super Admin', ]; + + return $map[$role] ?? ucfirst(str_replace('_', ' ', $role)); + } + + public function hasFuncao(string $funcao): bool + { + if ($funcao === 'super_admin') { + return $this->isSuperAdmin(); + } + + if (!$this->current_escola_id) { + return false; + } + + setPermissionsTeamId($this->current_escola_id); + + return $this->hasRole($funcao); } } diff --git a/app/Models/alunos.php b/app/Models/alunos.php deleted file mode 100644 index 4319757..0000000 --- a/app/Models/alunos.php +++ /dev/null @@ -1,17 +0,0 @@ -belongsTo(turmas::class, 'id_turma', 'id'); - } - - public function responsaveis() { - return $this->hasMany(User::class, 'id', 'id_responsavel'); - } -} diff --git a/app/Models/configs.php b/app/Models/configs.php new file mode 100644 index 0000000..8dcc787 --- /dev/null +++ b/app/Models/configs.php @@ -0,0 +1,10 @@ + 'array', - ]; - - protected $fillable = [ - 'cnpj', - 'endereco', - 'nome', - ]; - - public function turmas() - { - return $this->hasMany(turmas::class, 'id_escola', 'id'); - } -} diff --git a/app/Models/turmas.php b/app/Models/turmas.php deleted file mode 100644 index efc3216..0000000 --- a/app/Models/turmas.php +++ /dev/null @@ -1,26 +0,0 @@ -hasOne(escolas::class, 'id', 'id_escola'); - } - - public function alunos() - { - return $this->hasMany(alunos::class, 'id', 'id_turma'); - } - - public function enviosWpp() { - return $this->hasMany(envios_wpp::class, 'id_turma', 'id'); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..fdd399b 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,22 +3,20 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Gate; class AppServiceProvider extends ServiceProvider { - /** - * Register any application services. - */ public function register(): void { // } - /** - * Bootstrap any application services. - */ public function boot(): void { - // + // 🔥 Super Admin ignora todas as permissões + Gate::before(function ($user, $ability) { + return $user->is_super_admin ? true : null; + }); } -} +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..461a3b2 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Sentry\Laravel\Integration; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -11,8 +12,17 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + + // Aliases do Spatie (atenção: namespace é Middleware, singular) + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + ]); + + // ✅ Setar o "team" (id_escola) automaticamente em toda request autenticada + $middleware->append(\App\Http\Middleware\SetTenantMiddleware::class); }) ->withExceptions(function (Exceptions $exceptions): void { - // + Integration::handles($exceptions); })->create(); diff --git a/composer.json b/composer.json index 48e5c8b..d5dee8c 100755 --- a/composer.json +++ b/composer.json @@ -10,7 +10,10 @@ "laravel/breeze": "^2.3", "laravel/framework": "^12.0", "laravel/horizon": "^5.44", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "league/flysystem-aws-s3-v3": "^3.31", + "sentry/sentry-laravel": "^4.20", + "spatie/laravel-permission": "^7.1" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 7a91734..2be92f7 100755 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,159 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9898179818d19ab28a9c001b5b54d46e", + "content-hash": "e3b8f2c147faa7fbe05cdccc9a6eaafd", "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.369.38", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "6b39dcc5284fae1a1f7e5d4fd64878092a56594b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6b39dcc5284fae1a1f7e5d4fd64878092a56594b", + "reference": "6b39dcc5284fae1a1f7e5d4fd64878092a56594b", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^9.6", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.38" + }, + "time": "2026-02-19T19:06:55+00:00" + }, { "name": "brick/math", "version": "0.14.6", @@ -1052,6 +1203,66 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "laravel/breeze", "version": "v2.3.8", @@ -1872,6 +2083,61 @@ }, "time": "2026-01-23T15:38:47+00:00" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "e36a2bc60b06332c92e4435047797ded352b446f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/e36a2bc60b06332c92e4435047797ded352b446f", + "reference": "e36a2bc60b06332c92e4435047797ded352b446f", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.295.10", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, { "name": "league/flysystem-local", "version": "3.31.0", @@ -2262,6 +2528,72 @@ ], "time": "2026-01-02T08:56:05+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, { "name": "nesbot/carbon", "version": "3.11.1", @@ -2666,6 +2998,84 @@ ], "time": "2025-11-20T02:34:59+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -3430,6 +3840,332 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "sentry/sentry", + "version": "4.20.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "d7264b972e5f87110492376ade1cc20cbc049345" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/d7264b972e5f87110492376ade1cc20cbc049345", + "reference": "d7264b972e5f87110492376ade1cc20cbc049345", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0|^8.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.52|^9.6.34", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.20.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-02-16T13:47:54+00:00" + }, + { + "name": "sentry/sentry-laravel", + "version": "4.20.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-laravel.git", + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/503853fa7ee74b34b64e76f1373db86cd11afe72", + "reference": "503853fa7ee74b34b64e76f1373db86cd11afe72", + "shasum": "" + }, + "require": { + "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", + "nyholm/psr7": "^1.0", + "php": "^7.2 | ^8.0", + "sentry/sentry": "^4.19.0", + "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.11", + "guzzlehttp/guzzle": "^7.2", + "laravel/folio": "^1.1", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", + "laravel/pennant": "^1.0", + "livewire/livewire": "^2.0 | ^3.0", + "mockery/mockery": "^1.3", + "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4 | ^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Sentry": "Sentry\\Laravel\\Facade" + }, + "providers": [ + "Sentry\\Laravel\\ServiceProvider", + "Sentry\\Laravel\\Tracing\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-0": { + "Sentry\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "Laravel SDK for Sentry (https://sentry.io)", + "homepage": "https://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "laravel", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-laravel/issues", + "source": "https://github.com/getsentry/sentry-laravel/tree/4.20.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-01-07T08:53:19+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "7.1.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "34bf23c9e541a5434e0898c7172fb6647d23889d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/34bf23c9e541a5434e0898c7172fb6647d23889d", + "reference": "34bf23c9e541a5434e0898c7172fb6647d23889d", + "shasum": "" + }, + "require": { + "illuminate/auth": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/database": "^12.0", + "php": "^8.3", + "spatie/laravel-package-tools": "^1.0" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/passport": "^12.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/phpstan": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "7.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 12 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/7.1.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-14T17:17:28+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", @@ -3985,6 +4721,76 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-27T13:27:24+00:00" + }, { "name": "symfony/finder", "version": "v7.4.5", @@ -4427,6 +5233,77 @@ ], "time": "2026-01-27T08:59:58+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "b38026df55197f9e39a44f3215788edf83187b80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -5321,6 +6198,94 @@ ], "time": "2026-01-26T15:07:59+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/929ffe10bbfbb92e711ac3818d416f9daffee067", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-03T23:30:35+00:00" + }, { "name": "symfony/routing", "version": "v7.4.4", diff --git a/config/filesystems.php b/config/filesystems.php index 37d8fca..fcbfe2a 100755 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -41,7 +41,7 @@ return [ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/') . '/storage', 'visibility' => 'public', 'throw' => false, 'report' => false, @@ -51,13 +51,12 @@ return [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), - 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true), 'throw' => false, - 'report' => false, ], ], diff --git a/config/logging.php b/config/logging.php index 9e998a4..4b66a12 100755 --- a/config/logging.php +++ b/config/logging.php @@ -51,6 +51,12 @@ return [ */ 'channels' => [ + 'sentry_logs' => [ + 'driver' => 'sentry_logs', + // The minimum logging level at which this handler will be triggered + // Available levels: debug, info, notice, warning, error, critical, alert, emergency + 'level' => env('LOG_LEVEL', 'info'), // defaults to `debug` if not set + ], 'stack' => [ 'driver' => 'stack', @@ -89,7 +95,7 @@ return [ 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), - 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), ], 'processors' => [PsrLogMessageProcessor::class], ], diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..3cfb536 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'id_escola', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => true, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/config/sentry.php b/config/sentry.php new file mode 100644 index 0000000..abed25b --- /dev/null +++ b/config/sentry.php @@ -0,0 +1,135 @@ + env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')), + + // @see https://spotlightjs.com/ + // 'spotlight' => env('SENTRY_SPOTLIGHT', false), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#logger + // 'logger' => Sentry\Logger\DebugFileLogger::class, // By default this will log to `storage_path('logs/sentry.log')` + + // The release version of your application + // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) + 'release' => env('SENTRY_RELEASE'), + + // When left empty or `null` the Laravel environment will be used (usually discovered from `APP_ENV` in your `.env`) + 'environment' => env('SENTRY_ENVIRONMENT'), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#sample_rate + 'sample_rate' => env('SENTRY_SAMPLE_RATE') === null ? 1.0 : (float) env('SENTRY_SAMPLE_RATE'), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces_sample_rate + 'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_TRACES_SAMPLE_RATE'), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#profiles-sample-rate + 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_logs + 'enable_logs' => env('SENTRY_ENABLE_LOGS', false), + + // The minimum log level that will be sent to Sentry as logs using the `sentry_logs` logging channel + 'logs_channel_level' => env('SENTRY_LOG_LEVEL', env('SENTRY_LOGS_LEVEL', env('LOG_LEVEL', 'debug'))), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii + 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_exceptions + // 'ignore_exceptions' => [], + + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_transactions + 'ignore_transactions' => [ + // Ignore Laravel's default health URL + '/up', + ], + + // Breadcrumb specific configuration + 'breadcrumbs' => [ + // Capture Laravel logs as breadcrumbs + 'logs' => env('SENTRY_BREADCRUMBS_LOGS_ENABLED', true), + + // Capture Laravel cache events (hits, writes etc.) as breadcrumbs + 'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', true), + + // Capture Livewire components like routes as breadcrumbs + 'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', true), + + // Capture SQL queries as breadcrumbs + 'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true), + + // Capture SQL query bindings (parameters) in SQL query breadcrumbs + 'sql_bindings' => env('SENTRY_BREADCRUMBS_SQL_BINDINGS_ENABLED', false), + + // Capture queue job information as breadcrumbs + 'queue_info' => env('SENTRY_BREADCRUMBS_QUEUE_INFO_ENABLED', true), + + // Capture command information as breadcrumbs + 'command_info' => env('SENTRY_BREADCRUMBS_COMMAND_JOBS_ENABLED', true), + + // Capture HTTP client request information as breadcrumbs + 'http_client_requests' => env('SENTRY_BREADCRUMBS_HTTP_CLIENT_REQUESTS_ENABLED', true), + + // Capture send notifications as breadcrumbs + 'notifications' => env('SENTRY_BREADCRUMBS_NOTIFICATIONS_ENABLED', true), + ], + + // Performance monitoring specific configuration + 'tracing' => [ + // Trace queue jobs as their own transactions (this enables tracing for queue jobs) + 'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', true), + + // Capture queue jobs as spans when executed on the sync driver + 'queue_jobs' => env('SENTRY_TRACE_QUEUE_JOBS_ENABLED', true), + + // Capture SQL queries as spans + 'sql_queries' => env('SENTRY_TRACE_SQL_QUERIES_ENABLED', true), + + // Capture SQL query bindings (parameters) in SQL query spans + 'sql_bindings' => env('SENTRY_TRACE_SQL_BINDINGS_ENABLED', false), + + // Capture where the SQL query originated from on the SQL query spans + 'sql_origin' => env('SENTRY_TRACE_SQL_ORIGIN_ENABLED', true), + + // Define a threshold in milliseconds for SQL queries to resolve their origin + 'sql_origin_threshold_ms' => env('SENTRY_TRACE_SQL_ORIGIN_THRESHOLD_MS', 100), + + // Capture views rendered as spans + 'views' => env('SENTRY_TRACE_VIEWS_ENABLED', true), + + // Capture Livewire components as spans + 'livewire' => env('SENTRY_TRACE_LIVEWIRE_ENABLED', true), + + // Capture HTTP client requests as spans + 'http_client_requests' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ENABLED', true), + + // Capture Laravel cache events (hits, writes etc.) as spans + 'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true), + + // Capture Redis operations as spans (this enables Redis events in Laravel) + 'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false), + + // Capture where the Redis command originated from on the Redis command spans + 'redis_origin' => env('SENTRY_TRACE_REDIS_ORIGIN_ENABLED', true), + + // Capture send notifications as spans + 'notifications' => env('SENTRY_TRACE_NOTIFICATIONS_ENABLED', true), + + // Enable tracing for requests without a matching route (404's) + 'missing_routes' => env('SENTRY_TRACE_MISSING_ROUTES_ENABLED', false), + + // Configures if the performance trace should continue after the response has been sent to the user until the application terminates + // This is required to capture any spans that are created after the response has been sent like queue jobs dispatched using `dispatch(...)->afterResponse()` for example + 'continue_after_response' => env('SENTRY_TRACE_CONTINUE_AFTER_RESPONSE', true), + + // Enable the tracing integrations supplied by Sentry (recommended) + 'default_integrations' => env('SENTRY_TRACE_DEFAULT_INTEGRATIONS_ENABLED', true), + ], + +]; diff --git a/database/migrations/2026_02_15_180916_recreate_multi_tenant_structure.php b/database/migrations/2026_02_15_180916_recreate_multi_tenant_structure.php new file mode 100644 index 0000000..b4ace61 --- /dev/null +++ b/database/migrations/2026_02_15_180916_recreate_multi_tenant_structure.php @@ -0,0 +1,117 @@ +id(); + + $table->string('cnpj', 18)->unique(); + + $table->string('nome'); + + $table->string('endereco')->nullable(); + + $table->timestamps(); + + }); + + // users + Schema::create('users', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('id_escola') + ->nullable() + ->constrained('escolas') + ->nullOnDelete(); + + $table->string('name'); + + $table->string('email')->unique(); + + $table->string('password'); + + $table->boolean('is_super_admin')->default(false); + + $table->rememberToken(); + + $table->timestamps(); + + }); + + // turmas + Schema::create('turmas', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('id_escola') + ->constrained('escolas') + ->cascadeOnDelete(); + + $table->string('nome'); + + $table->text('descricao')->nullable(); + + $table->timestamps(); + + }); + + // alunos + Schema::create('alunos', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('id_escola') + ->constrained('escolas') + ->cascadeOnDelete(); + + $table->foreignId('id_turma') + ->constrained('turmas') + ->cascadeOnDelete(); + + $table->string('nome'); + + $table->string('cpf', 14)->nullable(); + + $table->date('data_nascimento')->nullable(); + + $table->date('data_inscricao')->nullable(); + + $table->date('data_encerramento')->nullable(); + + $table->string('endereco')->nullable(); + + $table->timestamps(); + + }); + + } + + public function down(): void + { + Schema::dropIfExists('alunos'); + Schema::dropIfExists('turmas'); + Schema::dropIfExists('users'); + Schema::dropIfExists('escolas'); + } +}; diff --git a/database/migrations/2026_02_15_181027_create_permission_tables.php b/database/migrations/2026_02_15_181027_create_permission_tables.php new file mode 100644 index 0000000..36f366f --- /dev/null +++ b/database/migrations/2026_02_15_181027_create_permission_tables.php @@ -0,0 +1,207 @@ +id(); + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + + }); + + + /* + |-------------------------------------------------------------------------- + | ROLES + |-------------------------------------------------------------------------- + */ + + Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $teamKey) { + + $table->id(); + + if ($teams) { + + $table->unsignedBigInteger($teamKey) + ->nullable(); + + $table->index($teamKey); + + } + + $table->string('name'); + + $table->string('guard_name'); + + $table->timestamps(); + + if ($teams) { + + $table->unique([$teamKey, 'name', 'guard_name']); + + } else { + + $table->unique(['name', 'guard_name']); + + } + + }); + + + /* + |-------------------------------------------------------------------------- + | MODEL HAS PERMISSIONS + |-------------------------------------------------------------------------- + */ + + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) + use ($tableNames, $columnNames, $pivotPermission, $teams, $teamKey) + { + + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + + $table->unsignedBigInteger($columnNames['model_morph_key']); + + if ($teams) { + + $table->unsignedBigInteger($teamKey) + ->nullable(); + + $table->index($teamKey); + + } + + $table->foreign($pivotPermission) + ->references('id') + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + + // primary SEM team_key + $table->primary([ + $pivotPermission, + $columnNames['model_morph_key'], + 'model_type' + ]); + + }); + + + /* + |-------------------------------------------------------------------------- + | MODEL HAS ROLES + |-------------------------------------------------------------------------- + */ + + Schema::create($tableNames['model_has_roles'], function (Blueprint $table) + use ($tableNames, $columnNames, $pivotRole, $teams, $teamKey) + { + + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + + $table->unsignedBigInteger($columnNames['model_morph_key']); + + if ($teams) { + + $table->unsignedBigInteger($teamKey) + ->nullable(); + + $table->index($teamKey); + + } + + $table->foreign($pivotRole) + ->references('id') + ->on($tableNames['roles']) + ->cascadeOnDelete(); + + // primary SEM team_key + $table->primary([ + $pivotRole, + $columnNames['model_morph_key'], + 'model_type' + ]); + + }); + + + /* + |-------------------------------------------------------------------------- + | ROLE HAS PERMISSIONS + |-------------------------------------------------------------------------- + */ + + Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) + use ($tableNames, $pivotRole, $pivotPermission) + { + + $table->unsignedBigInteger($pivotPermission); + + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + + $table->foreign($pivotRole) + ->references('id') + ->on($tableNames['roles']) + ->cascadeOnDelete(); + + $table->primary([ + $pivotPermission, + $pivotRole + ]); + + }); + + + app('cache') + ->store(config('permission.cache.store') != 'default' + ? config('permission.cache.store') + : null) + ->forget(config('permission.cache.key')); + + } + + + public function down(): void + { + + $tableNames = config('permission.table_names'); + + Schema::dropIfExists($tableNames['role_has_permissions']); + Schema::dropIfExists($tableNames['model_has_roles']); + Schema::dropIfExists($tableNames['model_has_permissions']); + Schema::dropIfExists($tableNames['roles']); + Schema::dropIfExists($tableNames['permissions']); + + } +}; diff --git a/database/migrations/2026_02_16_214604_create_user_escolas_table.php b/database/migrations/2026_02_16_214604_create_user_escolas_table.php new file mode 100644 index 0000000..65c6195 --- /dev/null +++ b/database/migrations/2026_02_16_214604_create_user_escolas_table.php @@ -0,0 +1,37 @@ +id(); + + $table->foreignId('user_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('escola_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_escolas'); + } +}; diff --git a/database/migrations/2026_02_16_214634_remove_id_escola_from_users.php b/database/migrations/2026_02_16_214634_remove_id_escola_from_users.php new file mode 100644 index 0000000..95d4828 --- /dev/null +++ b/database/migrations/2026_02_16_214634_remove_id_escola_from_users.php @@ -0,0 +1,30 @@ +dropConstrainedForeignId('id_escola'); + + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_02_16_214724_add_current_escola_to_users.php b/database/migrations/2026_02_16_214724_add_current_escola_to_users.php new file mode 100644 index 0000000..ba821cb --- /dev/null +++ b/database/migrations/2026_02_16_214724_add_current_escola_to_users.php @@ -0,0 +1,31 @@ +foreignId('current_escola_id') + ->nullable() + ->constrained('escolas'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_02_16_215213_rebuild_all_tables_multi_tenant.php b/database/migrations/2026_02_16_215213_rebuild_all_tables_multi_tenant.php new file mode 100644 index 0000000..3c2d4f4 --- /dev/null +++ b/database/migrations/2026_02_16_215213_rebuild_all_tables_multi_tenant.php @@ -0,0 +1,183 @@ +id(); + + $table->string('cnpj')->unique(); + + $table->string('nome'); + + $table->json('endereco')->nullable(); + + $table->timestamps(); + + }); + + + /* + |-------------------------------------------------------------------------- + | CRIAR USERS + |-------------------------------------------------------------------------- + */ + + Schema::create('users', function (Blueprint $table) { + + $table->id(); + + $table->string('name'); + + $table->string('email')->unique(); + + $table->string('password'); + + $table->boolean('is_super_admin') + ->default(false); + + $table->foreignId('current_escola_id') + ->nullable() + ->constrained('escolas') + ->nullOnDelete(); + + $table->rememberToken(); + + $table->timestamps(); + + }); + + + /* + |-------------------------------------------------------------------------- + | TABELA PIVOT USER_ESCOLAS + |-------------------------------------------------------------------------- + */ + + Schema::create('user_escolas', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('user_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('escola_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + + }); + + + /* + |-------------------------------------------------------------------------- + | TABELA TURMAS + |-------------------------------------------------------------------------- + */ + + Schema::create('turmas', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('id_escola') + ->constrained('escolas') + ->cascadeOnDelete(); + + $table->string('nome'); + + $table->text('descricao') + ->nullable(); + + $table->timestamps(); + + }); + + + /* + |-------------------------------------------------------------------------- + | TABELA ALUNOS + |-------------------------------------------------------------------------- + */ + + Schema::create('alunos', function (Blueprint $table) { + + $table->id(); + + $table->foreignId('id_escola') + ->constrained('escolas') + ->cascadeOnDelete(); + + $table->foreignId('id_turma') + ->constrained('turmas') + ->cascadeOnDelete(); + + $table->string('nome'); + + $table->string('cpf') + ->nullable(); + + $table->date('data_nascimento') + ->nullable(); + + $table->date('data_inscricao') + ->nullable(); + + $table->date('data_encerramento') + ->nullable(); + + $table->json('endereco') + ->nullable(); + + $table->timestamps(); + + }); + + } + + + public function down(): void + { + + Schema::disableForeignKeyConstraints(); + + Schema::dropIfExists('alunos'); + Schema::dropIfExists('turmas'); + Schema::dropIfExists('user_escolas'); + Schema::dropIfExists('users'); + Schema::dropIfExists('escolas'); + + Schema::enableForeignKeyConstraints(); + + } +}; diff --git a/database/migrations/2026_02_18_141220_add_wpp_turmas.php b/database/migrations/2026_02_18_141220_add_wpp_turmas.php new file mode 100644 index 0000000..210354f --- /dev/null +++ b/database/migrations/2026_02_18_141220_add_wpp_turmas.php @@ -0,0 +1,28 @@ +string('id_whatsapp')->unique()->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('turmas', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_02_19_190419_create_configs_table.php b/database/migrations/2026_02_19_190419_create_configs_table.php new file mode 100644 index 0000000..f6c5534 --- /dev/null +++ b/database/migrations/2026_02_19_190419_create_configs_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('nome')->unique(); + $table->string('valor')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('configs'); + } +}; diff --git a/database/migrations/2026_02_19_202026_add_user_links_to_alunos_table.php b/database/migrations/2026_02_19_202026_add_user_links_to_alunos_table.php new file mode 100644 index 0000000..9fb3a23 --- /dev/null +++ b/database/migrations/2026_02_19_202026_add_user_links_to_alunos_table.php @@ -0,0 +1,33 @@ +foreignId('user_id') + ->nullable() + ->after('id_turma') + ->constrained('users') + ->nullOnDelete(); + + $table->foreignId('responsavel_user_id') + ->nullable() + ->after('user_id') + ->constrained('users') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('alunos', function (Blueprint $table) { + $table->dropConstrainedForeignId('responsavel_user_id'); + $table->dropConstrainedForeignId('user_id'); + }); + } +}; diff --git a/database/migrations/2026_02_21_125534_create_aluno_responsaveis_table.php b/database/migrations/2026_02_21_125534_create_aluno_responsaveis_table.php new file mode 100644 index 0000000..153ce11 --- /dev/null +++ b/database/migrations/2026_02_21_125534_create_aluno_responsaveis_table.php @@ -0,0 +1,32 @@ +id(); + + $table->foreignId('aluno_id') + ->constrained('alunos') + ->cascadeOnDelete(); + + $table->foreignId('user_id') // responsável (users) + ->constrained('users') + ->cascadeOnDelete(); + + $table->timestamps(); + + // impede duplicar o mesmo responsável no mesmo aluno + $table->unique(['aluno_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('aluno_responsaveis'); + } +}; diff --git a/database/migrations/2026_02_21_125653_drop_responsavel_user_id_from_alunos_table.php b/database/migrations/2026_02_21_125653_drop_responsavel_user_id_from_alunos_table.php new file mode 100644 index 0000000..ceda63b --- /dev/null +++ b/database/migrations/2026_02_21_125653_drop_responsavel_user_id_from_alunos_table.php @@ -0,0 +1,28 @@ +dropConstrainedForeignId('responsavel_user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('alunos', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/seeders/InitialSeeder.php b/database/seeders/InitialSeeder.php new file mode 100644 index 0000000..fc4fc0e --- /dev/null +++ b/database/seeders/InitialSeeder.php @@ -0,0 +1,57 @@ +forgetCachedPermissions(); + + // Garante contexto GLOBAL (team = null) + setPermissionsTeamId(null); + + /** + * 1) CRIA ROLE GLOBAL super_admin (id_escola = NULL) + * OBS: como você está com teams/id_escola, deixamos explícito. + */ + $role = Role::firstOrCreate([ + 'name' => 'super_admin', + 'guard_name' => 'web', + 'id_escola' => null, + ]); + + /** + * 2) CRIA USUÁRIO SUPER ADMIN + */ + $user = User::firstOrCreate( + ['email' => 'admin@admin.com'], + [ + 'name' => 'Super Admin', + 'password' => bcrypt('123456'), + 'is_super_admin' => true, + 'current_escola_id' => null, + ] + ); + + /** + * 3) ATRIBUI A ROLE (GLOBAL) AO USUÁRIO + * Garante team = null novamente antes de gravar pivot. + */ + setPermissionsTeamId(null); + $user->syncRoles([$role]); + + // Limpa cache de novo por segurança + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + $this->command->info('Super Admin + role super_admin criados e associados com sucesso!'); + $this->command->info('Email: admin@admin.com'); + $this->command->info('Senha: 123456'); + } +} diff --git a/dump.rdb b/dump.rdb index f240cf8..524c128 100644 Binary files a/dump.rdb and b/dump.rdb differ diff --git a/public/assets/images/user-card/banner_user.png b/public/assets/images/user-card/banner_user.png new file mode 100644 index 0000000..59aa5f2 Binary files /dev/null and b/public/assets/images/user-card/banner_user.png differ diff --git a/python.py b/python.py new file mode 100644 index 0000000..6c5ac0a --- /dev/null +++ b/python.py @@ -0,0 +1,215 @@ +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import cm +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak +from reportlab.lib.enums import TA_CENTER, TA_LEFT + +# 1. Configuração do Arquivo +pdf_filename = "Apresentacao_Executiva_FGCOOP.pdf" +doc = SimpleDocTemplate(pdf_filename, pagesize=landscape(A4), + rightMargin=1*cm, leftMargin=1*cm, + topMargin=3.5*cm, bottomMargin=1.5*cm) + +# 2. Estilos +styles = getSampleStyleSheet() + +# Estilo de Título do Slide +style_slide_title = ParagraphStyle( + 'SlideTitle', + parent=styles['Heading1'], + fontSize=24, + textColor=colors.HexColor("#003366"), # Azul Corporativo + spaceAfter=20, + alignment=TA_LEFT +) + +# Estilo de Corpo do Slide +style_body = ParagraphStyle( + 'SlideBody', + parent=styles['Normal'], + fontSize=14, + leading=18, + spaceAfter=12, + textColor=colors.HexColor("#333333") +) + +# Estilo de Bullet Point +style_bullet = ParagraphStyle( + 'SlideBullet', + parent=style_body, + leftIndent=20, + bulletIndent=10, + firstLineIndent=0, + spaceAfter=8 +) + +# Estilo da Capa (Título Gigante) +style_cover_title = ParagraphStyle( + 'CoverTitle', + parent=styles['Title'], + fontSize=36, + textColor=colors.white, + alignment=TA_CENTER, + spaceAfter=20, + leading=40 +) + +style_cover_sub = ParagraphStyle( + 'CoverSub', + parent=styles['Normal'], + fontSize=18, + textColor=colors.lightgrey, + alignment=TA_CENTER +) + +# 3. Funções de Layout (Background) + +def draw_background(canvas, doc): + """ Fundo padrão dos slides (Barra azul no topo e rodapé) """ + width, height = landscape(A4) + + # Faixa Azul no Topo + canvas.saveState() + canvas.setFillColor(colors.HexColor("#003366")) + canvas.rect(0, height - 2.5*cm, width, 2.5*cm, fill=1, stroke=0) + + # Texto do Header (na faixa azul) + canvas.setFont("Helvetica-Bold", 16) + canvas.setFillColor(colors.white) + canvas.drawString(1*cm, height - 1.8*cm, "CLOUD ESSENTIAL | Assessment de Segurança") + + # Rodapé + canvas.setStrokeColor(colors.HexColor("#CCCCCC")) + canvas.setLineWidth(1) + canvas.line(1*cm, 1*cm, width-1*cm, 1*cm) + + # Texto do Rodapé + canvas.setFont("Helvetica", 10) + canvas.setFillColor(colors.gray) + canvas.drawString(1*cm, 0.5*cm, "Cliente: FGCOOP") + page_num = canvas.getPageNumber() + canvas.drawRightString(width-1*cm, 0.5*cm, f"Slide {page_num}") + + canvas.restoreState() + +def draw_cover_background(canvas, doc): + """ Fundo exclusivo da capa (Azul Total) """ + width, height = landscape(A4) + canvas.saveState() + + # Fundo Azul Sólido + canvas.setFillColor(colors.HexColor("#003366")) + canvas.rect(0, 0, width, height, fill=1, stroke=0) + + # Elemento decorativo (Círculo sutil) + canvas.setFillColor(colors.HexColor("#004080")) + canvas.circle(width/2, height/2, 12*cm, fill=1, stroke=0) + + # Logo simulado no centro inferior + canvas.setFont("Helvetica-Bold", 14) + canvas.setFillColor(colors.white) + canvas.drawCentredString(width/2, 2*cm, "CLOUD ESSENTIAL TECH") + + canvas.restoreState() + +# 4. Construção do Conteúdo (Story) +story = [] + +# --- SLIDE 1: CAPA --- +story.append(Spacer(1, 4*cm)) +story.append(Paragraph("Assessment de Infraestrutura
& Segurança Microsoft", style_cover_title)) +story.append(Spacer(1, 1.5*cm)) +story.append(Paragraph("Cliente: FGCOOP", style_cover_sub)) +story.append(Paragraph("Janeiro 2026", style_cover_sub)) +story.append(PageBreak()) + +# --- SLIDE 2: RESUMO EXECUTIVO --- +story.append(Paragraph("Resumo Executivo", style_slide_title)) + +# Texto com destaque +story.append(Paragraph("1. Visão Geral: Avaliação técnica dos ambientes On-Premises, Azure e M365 realizada em 19/01/2026.", style_body)) +story.append(Paragraph("2. Ponto Forte (Cloud): O ambiente possui um Microsoft Secure Score de 82,13%, superior à média de mercado (43%), indicando excelente maturidade em nuvem.", style_body)) +story.append(Paragraph("3. Ponto de Atenção (On-Prem): A infraestrutura local (AD e Servidores) apresenta riscos críticos que podem comprometer a segurança da nuvem se não tratados.", style_body)) +story.append(PageBreak()) + +# --- SLIDE 3: OTIMIZAÇÃO FINANCEIRA --- +story.append(Paragraph("Otimização de Custos (Licenciamento)", style_slide_title)) +story.append(Paragraph("Identificamos licenças ativas sem uso ou subutilizadas. Ação imediata recomendada:", style_body)) + +data_cost = [ + ['Produto / Licença', 'Situação Identificada', 'Recomendação'], + ['Microsoft 365 Business Basic', '44 Licenças não utilizadas', 'Cancelar ou realocar'], + ['Power BI Premium', 'Subutilização de recursos', 'Downgrade para versão Pro'], + ['Licenças F1', 'Baixo consumo', 'Revisar necessidade'] +] + +t_cost = Table(data_cost, colWidths=[7*cm, 8*cm, 8*cm]) +t_cost.setStyle(TableStyle([ + ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#004080")), + ('TEXTCOLOR', (0,0), (-1,0), colors.white), + ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), + ('BOTTOMPADDING', (0,0), (-1,0), 12), + ('BACKGROUND', (0,1), (-1,-1), colors.whitesmoke), + ('GRID', (0,0), (-1,-1), 1, colors.white), + ('ALIGN', (0,0), (-1,-1), 'LEFT'), + ('PADDING', (0,0), (-1,-1), 8), +])) +story.append(t_cost) +story.append(PageBreak()) + +# --- SLIDE 4: PONTOS FORTES --- +story.append(Paragraph("Destaques de Segurança (Pontos Fortes)", style_slide_title)) +story.append(Paragraph("A FGCOOP já implementou controles avançados de identidade:", style_body)) + +story.append(Paragraph("• Autenticação Passwordless: Uso de FIDO2 e Windows Hello, eliminando vetores de ataque baseados em senha.", style_bullet)) +story.append(Paragraph("• Proteção de Identidade: Bloqueio de senhas fracas no AD Local e proteção contra força bruta ativos.", style_bullet)) +story.append(Paragraph("• Acesso Condicional: Políticas maduras que bloqueiam autenticação legada e validam o risco do login.", style_bullet)) +story.append(PageBreak()) + +# --- SLIDE 5: RISCOS CRÍTICOS --- +story.append(Paragraph("Riscos Críticos Identificados", style_slide_title)) +story.append(Paragraph("Ativos que requerem intervenção imediata para evitar incidentes:", style_body)) + +data_risk = [ + ['Ativo', 'Nível', 'Impacto Técnico'], + ['Servidor SVM42000', 'CRÍTICO 🔴', 'Falha em Replicação, Sites e Certificados KDC'], + ['SVMAZDC01 / SVMAWSAD1', 'ALTO 🟠', 'Instabilidade de conectividade e sub-redes'], + ['Conta KRBTGT', 'ALTO 🟠', 'Senha nunca rotacionada (Risco de Golden Ticket)'], + ['Protocolo NTLM', 'MÉDIO 🟡', 'Protocolo vulnerável ativo na rede interna'] +] + +t_risk = Table(data_risk, colWidths=[6*cm, 3*cm, 14*cm]) +t_risk.setStyle(TableStyle([ + ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#CC0000")), + ('TEXTCOLOR', (0,0), (-1,0), colors.white), + ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), + ('ALIGN', (0,0), (-1,-1), 'LEFT'), + ('GRID', (0,0), (-1,-1), 0.5, colors.grey), + ('PADDING', (0,0), (-1,-1), 10), + ('BACKGROUND', (0,1), (-1,-1), colors.white), +])) +story.append(t_risk) +story.append(PageBreak()) + +# --- SLIDE 6: PLANO DE AÇÃO --- +story.append(Paragraph("Plano de Ação: Quick Wins (0-30 Dias)", style_slide_title)) +story.append(Paragraph("Ações prioritárias de baixo esforço e alto impacto:", style_body)) + +story.append(Paragraph("1. Saneamento do AD: Rotacionar senha da conta KRBTGT e corrigir delegações Kerberos.", style_bullet)) +story.append(Paragraph("2. Hardening de Servidores: Resolver erros de replicação no SVM42000 e habilitar assinatura LDAP.", style_bullet)) +story.append(Paragraph("3. Proteção de Endpoints: Aplicar regras de redução de superfície de ataque (ASR) via Intune.", style_bullet)) +story.append(Paragraph("4. Limpeza Financeira: Remover licenças Business Basic ociosas identificadas.", style_bullet)) +story.append(PageBreak()) + +# --- SLIDE 7: CONCLUSÃO --- +story.append(Paragraph("Conclusão e Próximos Passos", style_slide_title)) + +story.append(Paragraph("A FGCOOP está bem posicionada na nuvem (Score 82%), mas possui um 'débito técnico' perigoso na infraestrutura base (On-Premises).", style_body)) +story.append(Spacer(1, 1*cm)) +story.append(Paragraph("Recomendação Final: Priorizar a estabilização dos servidores de identidade (SVM42000) antes de iniciar novos projetos de inovação.", style_body)) +story.append(Spacer(1, 2*cm)) +story.append(Paragraph("Documento gerado por Cloud Essential Tech", style_body)) + +# 5. Gerar PDF +doc.build(story, onFirstPage=draw_cover_background, onLaterPages=draw_background) \ No newline at end of file diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php new file mode 100644 index 0000000..dbc0e70 --- /dev/null +++ b/resources/views/errors/403.blade.php @@ -0,0 +1,131 @@ + + + + + + + + + + + + {{env('APP_NAME')}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Oops! Parece que você não tem permissão de acesso.

+

Você está tentando acessar uma página que você não tem acesso, foi movida ou não existe mais. Entre em contato com suporte.

Voltar para o sistema +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/views/escolas/escola.detalhes.blade.php b/resources/views/escolas/escola.detalhes.blade.php new file mode 100644 index 0000000..75eb54d --- /dev/null +++ b/resources/views/escolas/escola.detalhes.blade.php @@ -0,0 +1,6 @@ +@extends('theme.default') +@section('title', 'Alunos') +@section('title.page', 'Alunos') +@section('title.page1', 'CAE') +@section('title.page2', 'Alunos') +@section('title.page3', 'Lista de alunos') \ No newline at end of file diff --git a/resources/views/escolas/index.blade.php b/resources/views/escolas/index.blade.php index bd65662..b26ba17 100644 --- a/resources/views/escolas/index.blade.php +++ b/resources/views/escolas/index.blade.php @@ -30,19 +30,27 @@ {{$escola->nome}} {{$escola->cnpj}} - Endereço: -

{{ $escola->endereco['street'] }} {{ $escola->endereco['number'] }} -

+ @if (isset($escola->endereco)) + Endereço: +

{{ $escola->endereco['street'] }} {{ $escola->endereco['number'] }}

+ @endif + - Complemento: -

{{ $escola->endereco['details'] }}

+ @if (isset($escola->endereco)) + Complemento: +

{{ $escola->endereco['details'] }}

+ @endif + - Cep: -

{{ $escola->endereco['zip'] }}

+ @if (isset($escola->endereco)) + Cep: +

{{ $escola->endereco['zip'] }}

+ @endif + diff --git a/resources/views/theme/default.blade.php b/resources/views/theme/default.blade.php index e56e6e4..22934b9 100644 --- a/resources/views/theme/default.blade.php +++ b/resources/views/theme/default.blade.php @@ -5,9 +5,9 @@ - - - {{ env('APP_NAME') }} - @yield('title') + + + @yield('title') - {{env('APP_NAME_SHORT')}} - + - + - + - + - + - + - - + + - + @php - use App\Models\escolas; - use App\Models\turmas; - if (Auth::user()->id_escola == 0) { - $escolas = escolas::all(); - } else { - $escolas = escolas::where('id', Auth::user()->id_escola)->get(); - } + use App\Models\Escola; + use App\Models\Turma; - if (Auth::user()->id_escola == 0) { - $turmas = turmas::all(); - } else { - $turmas = turmas::where('id_escola', Auth::user()->id_escola)->get(); - } + $escolas = Escola::all(); + $turmas = Turma::all(); @endphp
@@ -89,26 +81,36 @@
- + + @endrole + +
  • @csrf @@ -130,8 +132,8 @@