From e487aa8726780523c9c66a97ff2a45171b12b4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20de=20Jes=C3=BAs=20Cort=C3=A9s=20Castellanos?= <96152034+notsoweb@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:55:57 -0600 Subject: [PATCH] ADD: Historial de acciones (#5) --- app/Enums/EventTypeEk.php | 39 ++++++++ .../Controllers/Admin/ActivityController.php | 51 ++++++++++ app/Http/Controllers/Admin/UserController.php | 29 ++++++ .../Requests/Users/PasswordUpdateRequest.php | 2 +- .../Requests/Users/UserActivityRequest.php | 37 ++++++++ app/Models/User.php | 24 ++++- app/Models/UserEvent.php | 93 +++++++++++++++++++ app/Observers/UserObserver.php | 78 ++++++++++++++++ app/Providers/ObserverProvider.php | 34 +++++++ bootstrap/providers.php | 1 + composer.lock | 48 +++++----- config/app.php | 2 +- .../0001_01_01_000000_create_users_table.php | 1 + ..._12_30_173722_create_user_events_table.php | 35 +++++++ database/seeders/RoleSeeder.php | 13 ++- database/seeders/UserSeeder.php | 5 +- lang/es/user.php | 9 ++ routes/api.php | 32 ++++--- 18 files changed, 490 insertions(+), 43 deletions(-) create mode 100644 app/Enums/EventTypeEk.php create mode 100644 app/Http/Controllers/Admin/ActivityController.php create mode 100644 app/Http/Requests/Users/UserActivityRequest.php create mode 100644 app/Models/UserEvent.php create mode 100644 app/Observers/UserObserver.php create mode 100644 app/Providers/ObserverProvider.php create mode 100644 database/migrations/2024_12_30_173722_create_user_events_table.php create mode 100644 lang/es/user.php diff --git a/app/Enums/EventTypeEk.php b/app/Enums/EventTypeEk.php new file mode 100644 index 0000000..9bb4f34 --- /dev/null +++ b/app/Enums/EventTypeEk.php @@ -0,0 +1,39 @@ + + * + * @version 1.0.0 + */ +enum EventTypeEk : string +{ + use Extended; + + /** + * Texto + */ + case STRING = 'S'; + + /** + * JSON + */ + case JSON = 'J'; + + /** + * Booleano + */ + case BOOL = 'B'; + + /** + * Entero + */ + case INT = 'I'; +} diff --git a/app/Http/Controllers/Admin/ActivityController.php b/app/Http/Controllers/Admin/ActivityController.php new file mode 100644 index 0000000..c564521 --- /dev/null +++ b/app/Http/Controllers/Admin/ActivityController.php @@ -0,0 +1,51 @@ + + * + * @version 1.0.0 + */ +class ActivityController extends Controller +{ + /** + * Actividades del usuario + */ + public function index(UserActivityRequest $request) + { + $filters = $request->all(); + + $model = UserEvent::with('user:id,name,paternal,maternal,profile_photo_path,deleted_at'); + + if(isset($filters['user']) && !empty($filters['user'])){ + $model->where('user_id', $filters['user']); + } + + if(isset($filters['search']) && !empty($filters['search'])){ + $model->where('event', 'like', '%'.$filters['search'].'%'); + } + + if(isset($filters['start_date']) && !empty($filters['start_date'])){ + $model->where('created_at', '>=', "{$filters['start_date']} 00:00:00"); + } + + if(isset($filters['end_date']) && !empty($filters['end_date'])){ + $model->where('created_at', '<=', "{$filters['end_date']} 23:59:59"); + } + + return ApiResponse::OK->response([ + 'models' => + $model->orderBy('created_at', 'desc') + ->paginate(config('app.pagination')) + ]); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 8ba7ef7..f581dc1 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Users\PasswordUpdateRequest; +use App\Http\Requests\Users\UserActivityRequest; use App\Http\Requests\Users\UserStoreRequest; use App\Http\Requests\Users\UserUpdateRequest; use App\Models\User; @@ -128,4 +129,32 @@ public function updatePassword(PasswordUpdateRequest $request, User $user) return ApiResponse::OK->response(); } + + /** + * Actividades del usuario + */ + public function activity(UserActivityRequest $request, User $user) + { + $filters = $request->all(); + $model = $user->events() + ->with('user:id,name,paternal,maternal,profile_photo_path'); + + if($filters['search']){ + $model->where('event', 'like', '%'.$filters['search'].'%'); + } + + if($filters['start_date']){ + $model->where('created_at', '>=', "{$filters['start_date']} 00:00:00"); + } + + if($filters['end_date']){ + $model->where('created_at', '<=', "{$filters['end_date']} 23:59:59"); + } + + return ApiResponse::OK->response([ + 'models' => + $model->orderBy('created_at', 'desc') + ->paginate(config('app.pagination')) + ]); + } } diff --git a/app/Http/Requests/Users/PasswordUpdateRequest.php b/app/Http/Requests/Users/PasswordUpdateRequest.php index da560c0..8be99cc 100644 --- a/app/Http/Requests/Users/PasswordUpdateRequest.php +++ b/app/Http/Requests/Users/PasswordUpdateRequest.php @@ -19,7 +19,7 @@ class PasswordUpdateRequest extends FormRequest */ public function authorize(): bool { - return true; + return auth()->user()->hasPermissionTo('users.edit'); } /** diff --git a/app/Http/Requests/Users/UserActivityRequest.php b/app/Http/Requests/Users/UserActivityRequest.php new file mode 100644 index 0000000..760292d --- /dev/null +++ b/app/Http/Requests/Users/UserActivityRequest.php @@ -0,0 +1,37 @@ + + * + * @version 1.0.0 + */ +class UserActivityRequest extends FormRequest +{ + /** + * Determinar si el usuario está autorizado para realizar esta solicitud + */ + public function authorize(): bool + { + return true; + } + + /** + * Obtener las reglas de validación que se aplican a la solicitud + */ + public function rules(): array + { + return [ + 'search' => ['nullable', 'string', 'max:255'], + 'start_date' => ['nullable', 'date'], + 'end_date' => ['nullable', 'date'], + 'user' => ['nullable', 'exists:users,id'] + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 84dfd06..47b5a95 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,9 +8,11 @@ use App\Http\Traits\IsNotifiable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Support\Facades\Hash; use Laravel\Passport\HasApiTokens; +use Notsoweb\LaravelCore\Traits\Models\Extended; use Spatie\Permission\Traits\HasRoles; /** @@ -22,11 +24,13 @@ */ class User extends Authenticatable { - use HasApiTokens, + use Extended, + HasApiTokens, HasFactory, HasRoles, HasProfilePhoto, - IsNotifiable; + IsNotifiable, + SoftDeletes; /** * Atributos permitidos @@ -69,6 +73,22 @@ protected function casts(): array 'profile_photo_url', ]; + /** + * Un usuario puede generar muchos eventos + */ + public function events() + { + return $this->hasMany(UserEvent::class); + } + + /** + * Evento + */ + public function reports() + { + return $this->morphMany(UserEvent::class, 'reportable'); + } + /** * Nombre completo del usuario */ diff --git a/app/Models/UserEvent.php b/app/Models/UserEvent.php new file mode 100644 index 0000000..251acc9 --- /dev/null +++ b/app/Models/UserEvent.php @@ -0,0 +1,93 @@ + + * + * @version 1.0.0 + */ +class UserEvent extends Model +{ + /** + * Atributos que se pueden asignar masivamente + */ + protected $fillable = [ + 'event', + 'name', + 'data', + 'reportable_id', + 'reportable_type', + 'user_id' + ]; + + /** + * Transformaciones + */ + protected $casts = [ + 'data' => 'json' + ]; + + /** + * Atributos virtuales + */ + protected $appends = [ + 'description' + ]; + + /** + * Desactivar fecha actualización + */ + const UPDATED_AT = null; + + /** + * Relación con el usuario + */ + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + /** + * Descripción del evento + */ + public function description() : Attribute + { + return Attribute::make( + get: fn($value) => __($this->event, ['model' => $this->name]), + ); + } + + /** + * Relación con el modelo reportable + */ + public function reportable() + { + return $this->morphTo(); + } + + /** + * Reportar un evento + */ + public static function report(Model $model, string $event, string $key = 'name', bool $reportChanges = false) + { + $event = strtolower(explode('\\', get_class($model))[2]) . '.' . $event; + + self::create([ + 'event' => $event, + 'name' => $model->{$key}, + 'data' => $reportChanges ? $model->getContrastChanges() : $model->fillableToArray(), + 'reportable_id' => $model->id, + 'reportable_type' => get_class($model), + 'user_id' => auth()->user()?->id + ]); + } +} diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php new file mode 100644 index 0000000..34297e3 --- /dev/null +++ b/app/Observers/UserObserver.php @@ -0,0 +1,78 @@ + + * + * @version 1.0.0 + */ +class UserObserver +{ + /** + * Manipulador del evento "created" del modelo User + */ + public function created(User $user): void + { + UserEvent::report( + model: $user, + event: __FUNCTION__, + key: 'email' + ); + } + + /** + * Manipulador del evento "updated" del modelo User + */ + public function updated(User $user): void + { + UserEvent::report( + model: $user, + event: __FUNCTION__, + key: 'email', + reportChanges: true + ); + } + + /** + * Manipulador del evento "deleted" del modelo User + */ + public function deleted(User $user): void + { + UserEvent::report( + model: $user, + event: __FUNCTION__, + key: 'email' + ); + } + + /** + * Manipulador del evento "restored" del modelo User + */ + public function restored(User $user): void + { + UserEvent::report( + model: $user, + event: __FUNCTION__, + key: 'email' + ); + } + + /** + * Manipulador del evento "force deleted" del modelo User + */ + public function forceDeleted(User $user): void + { + UserEvent::report( + model: $user, + event: __FUNCTION__, + key: 'email' + ); + } +} diff --git a/app/Providers/ObserverProvider.php b/app/Providers/ObserverProvider.php new file mode 100644 index 0000000..df328fc --- /dev/null +++ b/app/Providers/ObserverProvider.php @@ -0,0 +1,34 @@ + + * + * @version 1.0.0 + */ +class ObserverProvider extends ServiceProvider +{ + /** + * Registrar servicios + */ + public function register(): void + { + // + } + + /** + * Inicializar servicios + */ + public function boot(): void + { + User::observe([UserObserver::class]); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 2064d0b..fd60e16 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,6 +2,7 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\ObserverProvider::class, Notsoweb\LaravelCore\ServiceProvider::class, Spatie\Permission\PermissionServiceProvider::class, ]; diff --git a/composer.lock b/composer.lock index 7ea75f4..ed79d48 100644 --- a/composer.lock +++ b/composer.lock @@ -2202,16 +2202,16 @@ }, { "name": "league/commonmark", - "version": "2.6.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d150f911e0079e90ae3c106734c93137c184f932" + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", - "reference": "d150f911e0079e90ae3c106734c93137c184f932", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", + "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", "shasum": "" }, "require": { @@ -2305,7 +2305,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T15:34:16+00:00" + "time": "2024-12-29T14:10:59+00:00" }, { "name": "league/config", @@ -3328,16 +3328,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -3380,9 +3380,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "notsoweb/api-response", @@ -3430,12 +3430,12 @@ "source": { "type": "git", "url": "https://github.com/notsoweb/laravel-core.git", - "reference": "97eac719b383e9e995827b8eeb17d325ded66b7e" + "reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/97eac719b383e9e995827b8eeb17d325ded66b7e", - "reference": "97eac719b383e9e995827b8eeb17d325ded66b7e", + "url": "https://api.github.com/repos/notsoweb/laravel-core/zipball/12da28db3a1b4634616fefa4bfb9fc7c277b7393", + "reference": "12da28db3a1b4634616fefa4bfb9fc7c277b7393", "shasum": "" }, "require": { @@ -3467,7 +3467,7 @@ "issues": "https://github.com/notsoweb/laravel-core/issues", "source": "https://github.com/notsoweb/laravel-core/tree/main" }, - "time": "2024-12-20T18:15:03+00:00" + "time": "2024-12-31T01:43:06+00:00" }, { "name": "nunomaduro/termwind", @@ -8393,16 +8393,16 @@ }, { "name": "laravel/pint", - "version": "v1.18.3", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026" + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026", - "reference": "cef51821608239040ab841ad6e1c6ae502ae3026", + "url": "https://api.github.com/repos/laravel/pint/zipball/8169513746e1bac70c85d6ea1524d9225d4886f0", + "reference": "8169513746e1bac70c85d6ea1524d9225d4886f0", "shasum": "" }, "require": { @@ -8413,10 +8413,10 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.65.0", - "illuminate/view": "^10.48.24", - "larastan/larastan": "^2.9.11", - "laravel-zero/framework": "^10.4.0", + "friendsofphp/php-cs-fixer": "^3.66.0", + "illuminate/view": "^10.48.25", + "larastan/larastan": "^2.9.12", + "laravel-zero/framework": "^10.48.25", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.17.0", "pestphp/pest": "^2.36.0" @@ -8455,7 +8455,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-11-26T15:34:00+00:00" + "time": "2024-12-30T16:20:10+00:00" }, { "name": "laravel/sail", diff --git a/config/app.php b/config/app.php index ceb2972..3b31eaf 100644 --- a/config/app.php +++ b/config/app.php @@ -12,7 +12,7 @@ | other UI elements where an application name needs to be displayed. | */ - 'version' => '0.9.4', + 'version' => '0.9.5', 'name' => env('APP_NAME', 'Laravel'), diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 45826fe..4b79dcb 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -24,6 +24,7 @@ public function up(): void $table->foreignId('current_team_id')->nullable(); $table->string('profile_photo_path', 2048)->nullable(); $table->timestamps(); + $table->softDeletes(); }); Schema::create('password_reset_tokens', function (Blueprint $table) { diff --git a/database/migrations/2024_12_30_173722_create_user_events_table.php b/database/migrations/2024_12_30_173722_create_user_events_table.php new file mode 100644 index 0000000..6f5c7af --- /dev/null +++ b/database/migrations/2024_12_30_173722_create_user_events_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('event'); + $table->string('name'); + $table->json('data')->nullable(); + $table->morphs('reportable'); + $table->foreignId('user_id') + ->nullable() + ->constrained() + ->nullOnDelete(); + $table->timestamp('created_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_events'); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index e1977ae..926b728 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -56,6 +56,16 @@ public function run(): void $systemPulse = $this->onPermission('pulse', 'Monitoreo de Pulse', $pulse, 'api'); + $pulse = PermissionType::create([ + 'name' => 'Historial de actividades' + ]); + + $activityIndex = $this->onIndex( + code: 'activities', + type: $pulse, + guardName: 'api' + ); + // Desarrollador Role::create([ 'name' => 'developer', @@ -79,7 +89,8 @@ public function run(): void $roleCreate, $roleEdit, $roleDestroy, - $systemPulse + $systemPulse, + $activityIndex ); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 3b4aedd..7da30a5 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -26,6 +26,7 @@ public function run(): void User::create([ 'name' => 'Developer', 'paternal' => 'Notsoweb', + 'maternal' => 'Software', 'email' => $developer->email, 'password' => $developer->hash, ])->assignRole(__('developer')); @@ -33,8 +34,9 @@ public function run(): void $admin = UserSecureSupport::create('admin@notsoweb.com'); User::create([ - 'name' => 'Developer', + 'name' => 'Admin', 'paternal' => 'Notsoweb', + 'maternal' => 'Software', 'email' => $admin->email, 'password' => $admin->hash, ])->assignRole(__('admin')); @@ -44,6 +46,7 @@ public function run(): void User::create([ 'name' => 'Demo', 'paternal' => 'Notsoweb', + 'maternal' => 'Software', 'email' => $demo->email, 'password' => $demo->hash, ]); diff --git a/lang/es/user.php b/lang/es/user.php new file mode 100644 index 0000000..0ae69c3 --- /dev/null +++ b/lang/es/user.php @@ -0,0 +1,9 @@ + 'El usuario :model ha sido creado', + 'updated' => 'El usuario :model ha sido actualizado', + 'deleted' => 'El usuario :model ha sido eliminado', + 'restored' => 'El usuario :model ha sido restaurado', +]; + diff --git a/routes/api.php b/routes/api.php index bcef25f..8fc8619 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ name('roles'); }); - Route::prefix('users')->name('users.')->group(function() { - Route::prefix('{user}')->group(function() { - Route::get('roles', [UserController::class, 'roles'])->name('roles'); - Route::put('roles', [UserController::class, 'updateRoles']); - Route::put('password', [UserController::class, 'updatePassword'])->name('password'); - Route::get('permissions', [UserController::class, 'permissions'])->name('permissions'); - }); - }); - Route::apiResource('users', UserController::class); + Route::prefix('admin')->name('admin.')->group(function() { + Route::apiResource('activities', ActivityController::class)->only(['index']); + + Route::prefix('users')->name('users.')->group(function() { + Route::prefix('{user}')->group(function() { + Route::get('roles', [UserController::class, 'roles'])->name('roles'); + Route::put('roles', [UserController::class, 'updateRoles']); + Route::put('password', [UserController::class, 'updatePassword'])->name('password'); + Route::get('permissions', [UserController::class, 'permissions'])->name('permissions'); + }); + }); + Route::apiResource('users', UserController::class); + + // Roles + Route::apiResource('roles', RoleController::class); + Route::get('roles/{role}/permissions', [RoleController::class, 'permissions'])->name('roles.permissions'); + Route::put('roles/{role}/permissions', [RoleController::class, 'updatePermissions'])->name('roles.permissions.update'); + }); - // Roles - Route::apiResource('roles', RoleController::class); - Route::get('roles/{role}/permissions', [RoleController::class, 'permissions'])->name('roles.permissions'); - Route::put('roles/{role}/permissions', [RoleController::class, 'updatePermissions'])->name('roles.permissions.update'); Route::prefix('permission-types')->name('permission-types.')->group(function() { Route::get('all', [PermissionTypeController::class, 'all'])->name('all');