Files
trafficpumper/app/Services/RoleAssignmentService.php
2026-08-29 15:53:53 +03:00

142 lines
7.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Enums\RoleName;
use App\Enums\UserStatus;
use App\Models\Role;
use App\Models\RoleAssignmentAudit;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Throwable;
class RoleAssignmentService
{
/**
* Kullanıcı bilgileriyle çoklu rol pivotunu atomik olarak günceller ve sonucu audit kaydına yazar.
*
* @param User $actor Değişikliği yapan kullanıcı.
* @param User $subject Bilgileri ve rolleri değiştirilecek kullanıcı.
* @param array<string, mixed> $attributes Güncellenecek güvenli kullanıcı alanları.
* @param array<int, int|string> $roleIds Seçili rollerin benzersizleştirilecek kimlikleri.
* @return User Güncel kullanıcı ve rol ilişkileri.
*
* @throws Throwable Yetkilendirme, doğrulama veya kalıcılık işlemi başarısız olduğunda.
*
* @author Gökhan Engin - İletişim : https://bit.ly/YazılımUzmanı
*/
public function update(User $actor, User $subject, array $attributes, array $roleIds): User
{
$correlationId = (string) Str::uuid();
$previousRoles = $subject->roles()->pluck('name')->sort()->values()->all();
$requestedRoles = $this->roleNamesForIds($roleIds);
try {
if (! $actor->can('update', $subject) || ! $actor->hasPermission('roles.assign')) {
throw new AuthorizationException;
}
return Cache::lock('super-admin-governance', 10)->block(5, fn () => Cache::lock('user-role-update:'.$subject->id, 10)->block(5, fn () => DB::transaction(function () use ($actor, $subject, $attributes, $roleIds, $correlationId) {
$locked = User::query()->lockForUpdate()->findOrFail($subject->id);
$roles = Role::query()->whereIn('id', array_values(array_unique($roleIds)))->get();
if ($roles->count() !== count(array_unique($roleIds))) {
throw ValidationException::withMessages(['data.roles' => __('roles.invalid')]);
}
if (($attributes['status'] ?? $locked->status->value) === UserStatus::Active->value && $roles->isEmpty()) {
throw ValidationException::withMessages(['data.roles' => __('roles.required')]);
}
$before = $locked->roles()->pluck('name')->sort()->values()->all();
$after = $roles->pluck('name')->sort()->values()->all();
$touchesSuperAdmin = in_array(RoleName::SuperAdmin->value, array_merge($before, $after), true);
if ($touchesSuperAdmin && ! $actor->hasRole(RoleName::SuperAdmin)) {
throw new AuthorizationException;
}
if ($actor->is($locked) && in_array(RoleName::SuperAdmin->value, $before, true) && ! in_array(RoleName::SuperAdmin->value, $after, true)) {
throw ValidationException::withMessages(['data.roles' => __('roles.self_remove')]);
}
$wouldDisableSuper = in_array(RoleName::SuperAdmin->value, $before, true) && (($attributes['status'] ?? $locked->status->value) !== UserStatus::Active->value || ! in_array(RoleName::SuperAdmin->value, $after, true));
if ($wouldDisableSuper && $this->activeSuperAdminCount($locked->id) === 0) {
throw ValidationException::withMessages(['data.roles' => __('roles.last_super_admin')]);
}
if ($actor->is($locked) && ($attributes['status'] ?? $locked->status->value) !== UserStatus::Active->value) {
throw ValidationException::withMessages(['data.status' => __('roles.self_suspend')]);
}
$locked->fill($attributes)->save();
$locked->roles()->sync($roles->modelKeys());
RoleAssignmentAudit::create([
'actor_user_id' => $actor->id, 'subject_user_id' => $locked->id, 'correlation_id' => $correlationId,
'previous_roles' => $before, 'new_roles' => $after, 'added_roles' => array_values(array_diff($after, $before)),
'removed_roles' => array_values(array_diff($before, $after)), 'result' => 'success',
]);
return $locked->refresh();
}, 3)));
} catch (Throwable $exception) {
RoleAssignmentAudit::create([
'actor_user_id' => $actor->id, 'subject_user_id' => $subject->id, 'correlation_id' => $correlationId,
'previous_roles' => $previousRoles, 'new_roles' => $requestedRoles,
'added_roles' => array_values(array_diff($requestedRoles, $previousRoles)),
'removed_roles' => array_values(array_diff($previousRoles, $requestedRoles)), 'result' => 'failure',
]);
throw $exception;
}
}
/**
* İstemciden gelen rol kimliklerini mevcut rol kayıtlarındaki sıralı adlara dönüştürür.
*
* @param array<int, int|string> $roleIds Çözümlenecek rol kimlikleri.
* @return array<int, string> Veritabanında bulunan benzersiz rol adları.
*
* @author Gökhan Engin - İletişim : https://bit.ly/YazılımUzmanı
*/
private function roleNamesForIds(array $roleIds): array
{
return Role::query()->whereIn('id', array_values(array_unique($roleIds)))
->pluck('name')->sort()->values()->all();
}
public function replaceRole(User $actor, User $subject, RoleName|string $role): void
{
$name = $role instanceof RoleName ? $role->value : $role;
$this->update($actor, $subject, [], [Role::where('name', $name)->sole()->id]);
}
public function delete(User $actor, User $subject): void
{
if ($actor->is($subject)) {
throw ValidationException::withMessages(['user' => __('roles.self_delete')]);
}
if (! $actor->can('delete', $subject)) {
throw new AuthorizationException;
}
Cache::lock('super-admin-governance', 10)->block(5, fn () => DB::transaction(function () use ($actor, $subject) {
$locked = User::query()->lockForUpdate()->findOrFail($subject->id);
if (! $actor->can('delete', $locked)) {
throw new AuthorizationException;
}
if ($locked->hasRole(RoleName::SuperAdmin) && $this->activeSuperAdminCount($locked->id) === 0) {
throw ValidationException::withMessages(['user' => __('roles.last_super_admin_delete')]);
}
DB::table('sessions')->where('user_id', $locked->id)->delete();
$locked->tokens()->delete();
$locked->forceFill(['remember_token' => Str::random(60)])->saveQuietly();
$locked->delete();
}, 3));
}
private function activeSuperAdminCount(int $exceptId): int
{
return User::query()->where('id', '!=', $exceptId)->where('status', UserStatus::Active->value)
->whereHas('roles', fn ($q) => $q->where('name', RoleName::SuperAdmin->value))->lockForUpdate()->count();
}
}