270 lines
8.6 KiB
PHP
270 lines
8.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Events\UserDeactivated;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AuditLog;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Validation\Rule;
|
|
use Spatie\Permission\Models\Role;
|
|
|
|
class UserController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
return response()->json(
|
|
User::query()->whereNotNull('approved_at')->orderBy('name')->get()->map($this->present(...))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Usuarios borrados (soft delete) — la "papelera".
|
|
*/
|
|
public function trashed()
|
|
{
|
|
return response()->json(
|
|
User::onlyTrashed()->orderByDesc('deleted_at')->get()->map($this->present(...))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Usuarios que se autorregistraron y todavía no aprobó ningún admin.
|
|
*/
|
|
public function pending()
|
|
{
|
|
return response()->json(
|
|
User::query()->whereNull('approved_at')->orderBy('created_at')->get()->map($this->present(...))
|
|
);
|
|
}
|
|
|
|
public function approve(User $user)
|
|
{
|
|
$user->update(['approved_at' => now()]);
|
|
|
|
AuditLog::record(request(), 'user.approved', 'user', $user->id, $user->email);
|
|
|
|
return response()->json($this->present($user));
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$data = $request->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')],
|
|
'password' => ['required', 'string', 'min:8'],
|
|
'role' => ['required', Rule::in(Role::pluck('name'))],
|
|
]);
|
|
|
|
$user = User::create([
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'password' => Hash::make($data['password']),
|
|
'is_active' => true,
|
|
'approved_at' => now(), // lo da de alta un admin: ya está aprobado.
|
|
]);
|
|
|
|
$user->assignRole($data['role']);
|
|
|
|
AuditLog::record($request, 'user.created', 'user', $user->id, $user->email, [
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'role' => $data['role'],
|
|
]);
|
|
|
|
return response()->json($this->present($user), 201);
|
|
}
|
|
|
|
public function update(Request $request, User $user)
|
|
{
|
|
$data = $request->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)],
|
|
'password' => ['nullable', 'string', 'min:8'],
|
|
'is_active' => ['boolean'],
|
|
'role' => ['required', Rule::in(Role::pluck('name'))],
|
|
]);
|
|
|
|
if ($user->id === $request->user()->id && $data['role'] !== 'admin' && $user->hasRole('admin')) {
|
|
abort(422, __('messages.cannot_remove_own_admin_role'));
|
|
}
|
|
|
|
$before = [
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'is_active' => $user->is_active,
|
|
'role' => $user->getRoleNames()->first(),
|
|
];
|
|
|
|
$user->update([
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'is_active' => $data['is_active'] ?? $user->is_active,
|
|
...(isset($data['password']) ? ['password' => Hash::make($data['password'])] : []),
|
|
]);
|
|
|
|
$user->syncRoles([$data['role']]);
|
|
|
|
$fresh = $user->fresh();
|
|
|
|
if ($before['is_active'] && ! $fresh->is_active) {
|
|
UserDeactivated::dispatch($fresh);
|
|
}
|
|
|
|
$this->recordUserUpdate($request, $fresh, $before, isset($data['password']));
|
|
|
|
return response()->json($this->present($fresh));
|
|
}
|
|
|
|
/**
|
|
* Soft delete: el usuario deja de aparecer en el sistema pero queda
|
|
* recuperable desde la papelera.
|
|
*/
|
|
public function destroy(Request $request, User $user)
|
|
{
|
|
abort_if($user->id === $request->user()->id, 422, __('messages.cannot_delete_self'));
|
|
|
|
if ($user->hasRole('admin') && User::role('admin')->count() <= 1) {
|
|
abort(422, __('messages.cannot_delete_last_admin'));
|
|
}
|
|
|
|
AuditLog::record($request, 'user.deleted', 'user', $user->id, $user->email);
|
|
|
|
$user->delete();
|
|
|
|
return response()->noContent();
|
|
}
|
|
|
|
public function restore(int $id)
|
|
{
|
|
$user = User::onlyTrashed()->findOrFail($id);
|
|
$user->restore();
|
|
|
|
AuditLog::record(request(), 'user.restored', 'user', $user->id, $user->email);
|
|
|
|
return response()->json($this->present($user));
|
|
}
|
|
|
|
/**
|
|
* Borrado definitivo — solo tiene sentido sobre alguien que ya está en la papelera.
|
|
*/
|
|
public function forceDelete(Request $request, int $id)
|
|
{
|
|
$user = User::onlyTrashed()->findOrFail($id);
|
|
|
|
AuditLog::record($request, 'user.force_deleted', 'user', $user->id, $user->email);
|
|
|
|
$user->forceDelete();
|
|
|
|
return response()->noContent();
|
|
}
|
|
|
|
/**
|
|
* Roles que este usuario tiene asignados en cada app (cliente OAuth), agrupados.
|
|
* Es un sistema aparte del rol global de spatie: vive en app_user_roles,
|
|
* no en las tablas de spatie (evita mezclar "quién administra este panel"
|
|
* con "qué rol tiene este usuario dentro de la app de un tercero").
|
|
*/
|
|
public function appRoles(User $user)
|
|
{
|
|
$rows = DB::table('app_user_roles')
|
|
->join('oauth_clients', 'oauth_clients.id', '=', 'app_user_roles.client_id')
|
|
->where('app_user_roles.user_id', $user->id)
|
|
->get(['oauth_clients.id as client_id', 'oauth_clients.name as client_name', 'app_user_roles.role']);
|
|
|
|
return response()->json(
|
|
$rows->groupBy('client_id')->map(fn ($group) => [
|
|
'client_id' => $group->first()->client_id,
|
|
'client_name' => $group->first()->client_name,
|
|
'roles' => $group->pluck('role'),
|
|
])->values()
|
|
);
|
|
}
|
|
|
|
public function assignAppRole(Request $request, User $user)
|
|
{
|
|
$data = $request->validate([
|
|
'client_id' => ['required', 'exists:oauth_clients,id'],
|
|
'role' => ['required', Rule::in(Role::pluck('name'))],
|
|
]);
|
|
|
|
DB::table('app_user_roles')->insertOrIgnore([
|
|
'user_id' => $user->id,
|
|
'client_id' => $data['client_id'],
|
|
'role' => $data['role'],
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
AuditLog::record($request, 'user.app_role_assigned', 'user', $user->id, $user->email, [
|
|
'client_id' => $data['client_id'],
|
|
'role' => $data['role'],
|
|
]);
|
|
|
|
return response()->json($data, 201);
|
|
}
|
|
|
|
public function revokeAppRole(User $user, string $clientId, string $role)
|
|
{
|
|
DB::table('app_user_roles')
|
|
->where('user_id', $user->id)
|
|
->where('client_id', $clientId)
|
|
->where('role', $role)
|
|
->delete();
|
|
|
|
AuditLog::record(request(), 'user.app_role_revoked', 'user', $user->id, $user->email, [
|
|
'client_id' => $clientId,
|
|
'role' => $role,
|
|
]);
|
|
|
|
return response()->noContent();
|
|
}
|
|
|
|
/**
|
|
* Solo registra los campos que efectivamente cambiaron (nunca la contraseña
|
|
* en sí, solo si cambió o no) — evita ruido en el audit log en updates que
|
|
* no tocan nada relevante.
|
|
*/
|
|
private function recordUserUpdate(Request $request, User $user, array $before, bool $passwordChanged): void
|
|
{
|
|
$after = [
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'is_active' => $user->is_active,
|
|
'role' => $user->getRoleNames()->first(),
|
|
];
|
|
|
|
$changes = [];
|
|
foreach ($before as $field => $previous) {
|
|
if ($previous !== $after[$field]) {
|
|
$changes[$field] = ['from' => $previous, 'to' => $after[$field]];
|
|
}
|
|
}
|
|
|
|
if ($passwordChanged) {
|
|
$changes['password'] = ['changed' => true];
|
|
}
|
|
|
|
if ($changes !== []) {
|
|
AuditLog::record($request, 'user.updated', 'user', $user->id, $user->email, $changes);
|
|
}
|
|
}
|
|
|
|
private function present(User $user): array
|
|
{
|
|
return [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
'is_active' => $user->is_active,
|
|
'roles' => $user->getRoleNames(),
|
|
'created_at' => $user->created_at,
|
|
'deleted_at' => $user->deleted_at,
|
|
'approved_at' => $user->approved_at,
|
|
];
|
|
}
|
|
}
|