142 lines
7.1 KiB
PHP
142 lines
7.1 KiB
PHP
<?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();
|
|||
|
|
}
|
|||
|
|
}
|