[]]], tags: ['Users'], responses: [ new OA\Response(response: 200, description: 'Usuario autenticado', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 401, description: 'Token inválido o sin usuario asociado (ej. client_credentials)'), ] )] public function me(Request $request) { return response()->json($this->present($request->user())); } #[OA\Get( path: '/users', summary: 'Listar usuarios', description: 'Incluye usuarios pendientes de aprobación del panel admin (ver campo approved_at) — esa aprobación solo gobierna el acceso al panel de sesión de esta app, no la consulta de usuarios vía API.', security: [['oauth2' => ['users:read']]], tags: ['Users'], responses: [ new OA\Response(response: 200, description: 'Listado de usuarios', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/User'))), new OA\Response(response: 403, description: 'Sin permiso users.view'), ] )] public function index(Request $request) { $this->authorize($request, 'users.view'); // A diferencia del panel admin (Admin\UserController), acá NO se // esconden los usuarios pendientes de aprobación: "aprobado" es un // concepto del panel de esta app (¿puede entrar a /admin?), no algo // que le importe a otra app que solo está consultando/consumiendo // usuarios por la API. return response()->json( User::query()->with('roles:id,name')->orderBy('name')->get()->map($this->present(...)) ); } #[OA\Post( path: '/users', summary: 'Crear un usuario', security: [['oauth2' => ['users:write']]], tags: ['Users'], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['name', 'email', 'password'], properties: [ new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'email', type: 'string', format: 'email'), new OA\Property(property: 'password', type: 'string', format: 'password'), new OA\Property(property: 'role', type: 'string', example: 'user'), ] ) ), responses: [ new OA\Response(response: 201, description: 'Usuario creado', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 403, description: 'Sin permiso users.create'), ] )] public function store(Request $request) { $this->authorize($request, 'users.create'); $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')], 'password' => ['required', 'string', 'min:8'], 'role' => ['sometimes', 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 crea un admin/servicio de confianza: ya está aprobado. ]); $user->assignRole($data['role'] ?? 'user'); return response()->json($this->present($user), 201); } #[OA\Get( path: '/users/{user}', summary: 'Ver un usuario (el propio siempre permitido; otros requieren users.view)', security: [['oauth2' => ['users:read']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [ new OA\Response(response: 200, description: 'Datos del usuario', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 403, description: 'Sin permiso users.view'), ] )] public function show(Request $request, User $user) { $this->authorize($request, 'users.view', $user); return response()->json($this->present($user)); } #[OA\Put( path: '/users/{user}', summary: 'Editar un usuario (el propio siempre permitido; rol/estado y otros usuarios requieren users.update)', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( properties: [ new OA\Property(property: 'name', type: 'string'), new OA\Property(property: 'email', type: 'string', format: 'email'), new OA\Property(property: 'password', type: 'string', format: 'password'), new OA\Property(property: 'is_active', type: 'boolean'), new OA\Property(property: 'role', type: 'string'), ] ) ), responses: [ new OA\Response(response: 200, description: 'Usuario actualizado', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 403, description: 'Sin permiso users.update'), ] )] public function update(Request $request, User $user) { $actor = $this->authorize($request, 'users.update', $user); $isAdminAction = is_null($actor) || $actor->can('users.update'); $data = $request->validate([ 'name' => ['sometimes', 'string', 'max:255'], 'email' => ['sometimes', 'string', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)], 'password' => ['sometimes', 'string', 'min:8'], 'is_active' => ['sometimes', 'boolean'], 'role' => ['sometimes', Rule::in(Role::pluck('name'))], ]); if (! $isAdminAction) { $data = array_intersect_key($data, array_flip(['name', 'email', 'password'])); } if (isset($data['role']) && $user->id === $actor?->id && $data['role'] !== 'admin' && $user->hasRole('admin')) { abort(422, __('messages.cannot_remove_own_admin_role')); } $wasActive = $user->is_active; $user->update([ ...array_diff_key($data, array_flip(['password', 'role'])), ...(isset($data['password']) ? ['password' => Hash::make($data['password'])] : []), ]); if (isset($data['role'])) { $user->syncRoles([$data['role']]); } $fresh = $user->fresh(); if ($wasActive && ! $fresh->is_active) { UserDeactivated::dispatch($fresh); } return response()->json($this->present($fresh)); } #[OA\Delete( path: '/users/{user}', summary: 'Borrar un usuario (requiere users.delete; no se puede autoeliminar)', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [ new OA\Response(response: 204, description: 'Usuario borrado'), new OA\Response(response: 403, description: 'Sin permiso users.delete'), new OA\Response(response: 422, description: 'No se puede autoeliminar ni borrar al último admin'), ] )] public function destroy(Request $request, User $user) { $actor = $request->user('api'); abort_if($actor && $actor->id === $user->id, 422, __('messages.cannot_delete_self')); abort_if($actor && ! $actor->can('users.delete'), 403); if ($user->hasRole('admin') && User::role('admin')->count() <= 1) { abort(422, __('messages.cannot_delete_last_admin')); } $user->delete(); return response()->noContent(); } #[OA\Get( path: '/users/trashed', summary: 'Listar usuarios borrados (papelera)', security: [['oauth2' => ['users:read']]], tags: ['Users'], responses: [ new OA\Response(response: 200, description: 'Listado de usuarios en la papelera', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/User'))), new OA\Response(response: 403, description: 'Sin permiso users.view'), ] )] public function trashed(Request $request) { $this->authorize($request, 'users.view'); return response()->json( User::onlyTrashed()->orderByDesc('deleted_at')->get()->map($this->present(...)) ); } #[OA\Post( path: '/users/{id}/restore', summary: 'Restaurar un usuario borrado', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [ new OA\Response(response: 200, description: 'Usuario restaurado', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 403, description: 'Sin permiso users.update'), ] )] public function restore(Request $request, int $id) { $actor = $request->user('api'); abort_if($actor && ! $actor->can('users.update'), 403); $user = User::onlyTrashed()->findOrFail($id); $user->restore(); return response()->json($this->present($user)); } #[OA\Delete( path: '/users/{id}/force', summary: 'Borrar un usuario definitivamente (tiene que estar en la papelera)', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [ new OA\Response(response: 204, description: 'Borrado definitivamente'), new OA\Response(response: 403, description: 'Sin permiso users.delete'), ] )] public function forceDelete(Request $request, int $id) { $actor = $request->user('api'); abort_if($actor && ! $actor->can('users.delete'), 403); $user = User::onlyTrashed()->findOrFail($id); $user->forceDelete(); return response()->noContent(); } #[OA\Get( path: '/users/pending', summary: 'Listar usuarios autorregistrados que todavía no aprobó ningún admin', security: [['oauth2' => ['users:read']]], tags: ['Users'], responses: [ new OA\Response(response: 200, description: 'Listado de usuarios pendientes', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/User'))), new OA\Response(response: 403, description: 'Sin permiso users.view'), ] )] public function pending(Request $request) { $this->authorize($request, 'users.view'); return response()->json( User::query()->whereNull('approved_at')->orderBy('created_at')->get()->map($this->present(...)) ); } #[OA\Post( path: '/users/{user}/approve', summary: 'Aprobar un registro pendiente', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [ new OA\Response(response: 200, description: 'Usuario aprobado', content: new OA\JsonContent(ref: '#/components/schemas/User')), new OA\Response(response: 403, description: 'Sin permiso users.update'), ] )] public function approve(Request $request, User $user) { $actor = $request->user('api'); abort_if($actor && ! $actor->can('users.update'), 403); $user->update(['approved_at' => now()]); return response()->json($this->present($user)); } #[OA\Get( path: '/users/{user}/app-roles', summary: 'Roles que el usuario tiene en cada app (el propio siempre permitido; otros requieren users.view)', security: [['oauth2' => ['users:read']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], responses: [new OA\Response(response: 200, description: 'Roles agrupados por app')] )] public function appRoles(Request $request, User $user) { $this->authorize($request, 'users.view', $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() ); } #[OA\Post( path: '/users/{user}/app-roles', summary: 'Asignar un rol a un usuario dentro de una app (requiere users.update; sin bypass por self)', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))], requestBody: new OA\RequestBody( required: true, content: new OA\JsonContent( required: ['client_id', 'role'], properties: [ new OA\Property(property: 'client_id', type: 'string', format: 'uuid'), new OA\Property(property: 'role', type: 'string', example: 'admin'), ] ) ), responses: [ new OA\Response(response: 201, description: 'Rol asignado'), new OA\Response(response: 403, description: 'Sin permiso users.update'), ] )] public function assignAppRole(Request $request, User $user) { $actor = $request->user('api'); abort_if($actor && ! $actor->can('users.update'), 403); $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(), ]); return response()->json($data, 201); } #[OA\Delete( path: '/users/{user}/app-roles/{clientId}/{role}', summary: 'Quitarle a un usuario un rol dentro de una app (requiere users.update)', security: [['oauth2' => ['users:write']]], tags: ['Users'], parameters: [ new OA\Parameter(name: 'user', in: 'path', required: true, schema: new OA\Schema(type: 'integer')), new OA\Parameter(name: 'clientId', in: 'path', required: true, schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'role', in: 'path', required: true, schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response(response: 204, description: 'Rol quitado'), new OA\Response(response: 403, description: 'Sin permiso users.update'), ] )] public function revokeAppRole(Request $request, User $user, string $clientId, string $role) { $actor = $request->user('api'); abort_if($actor && ! $actor->can('users.update'), 403); DB::table('app_user_roles') ->where('user_id', $user->id) ->where('client_id', $clientId) ->where('role', $role) ->delete(); return response()->noContent(); } /** * Un token client_credentials (sin usuario) actúa como servicio de confianza: * ya validado por scope, no necesita permiso de usuario. Si hay usuario, puede * operar siempre sobre sí mismo; sobre otros usuarios necesita el permiso dado. */ private function authorize(Request $request, string $permission, ?User $target = null): ?User { $actor = $request->user('api'); if (! $actor) { return null; } $isSelf = $target && $target->id === $actor->id; if (! $isSelf && ! $actor->can($permission)) { abort(403); } return $actor; } /** * Devuelve el usuario más "app_roles": los roles que tiene asignados * específicamente para la app que está llamando (el cliente OAuth del * token actual), guardados en app_user_roles — un sistema aparte del rol * global de spatie (ver Admin\UserController::appRoles). */ private function present(User $user): array { $callingClientId = Auth::guard('api')->client()?->id; $appRoles = $callingClientId ? DB::table('app_user_roles') ->where('user_id', $user->id) ->where('client_id', $callingClientId) ->pluck('role') : collect(); return [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'is_active' => $user->is_active, 'roles' => $user->getRoleNames(), 'app_roles' => $appRoles, 'created_at' => $user->created_at, 'approved_at' => $user->approved_at, ]; } }