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, ]; } }