From f527ca932a65950143720aad8d5afecd406d9859 Mon Sep 17 00:00:00 2001 From: ok200lyndon Date: Thu, 15 Aug 2024 16:53:03 +1000 Subject: [PATCH 01/15] audit items tests, controllers and UI --- app/Console/Commands/TestCommand.php | 4 + app/Enums/PersonalAccessTokenAbility.php | 30 ++-- .../V1/Admin/ApiAdminAuditItemsController.php | 101 ++++++++++++ .../Api/V1/Admin/ApiAdminSearchController.php | 4 +- .../Api/V1/ApiMyTeamAuditItemsController.php | 98 ++++++++++++ app/Models/AuditItem.php | 24 +++ app/Models/Team.php | 6 + app/Models/User.php | 6 + database/factories/AuditItemFactory.php | 47 ++++++ ..._08_14_052516_create_audit_items_table.php | 36 +++++ package-lock.json | 14 +- package.json | 3 + .../js/Components/AuditItemsComponent.vue | 41 +++++ resources/js/Layouts/AuthenticatedLayout.vue | 6 + resources/js/Pages/AuditItems.vue | 78 +++++++++ routes/api.php | 56 +++++++ routes/web.php | 8 + .../AdminAuditItemPostTest.php | 47 ++++++ .../AdminAuditItems/AdminAuditItemPutTest.php | 48 ++++++ .../AdminAuditItemsDeleteTest.php | 49 ++++++ .../AdminAuditItemsGetTest.php | 67 ++++++++ .../API/App/AuditItems/AuditItemPostTest.php | 77 +++++++++ .../API/App/AuditItems/AuditItemPutTest.php | 77 +++++++++ .../App/AuditItems/AuditItemsDeleteTest.php | 78 +++++++++ .../API/App/AuditItems/AuditItemsGetTest.php | 149 ++++++++++++++++++ tests/Unit/Models/AuditItemTest.php | 86 ++++++++++ tests/Unit/Models/TeamTest.php | 36 +++++ tests/Unit/Models/UserTest.php | 36 +++++ 28 files changed, 1295 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php create mode 100644 app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php create mode 100644 app/Models/AuditItem.php create mode 100644 database/factories/AuditItemFactory.php create mode 100644 database/migrations/2024_08_14_052516_create_audit_items_table.php create mode 100644 resources/js/Components/AuditItemsComponent.vue create mode 100644 resources/js/Pages/AuditItems.vue create mode 100644 tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPostTest.php create mode 100644 tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPutTest.php create mode 100644 tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsDeleteTest.php create mode 100644 tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsGetTest.php create mode 100644 tests/Feature/API/App/AuditItems/AuditItemPostTest.php create mode 100644 tests/Feature/API/App/AuditItems/AuditItemPutTest.php create mode 100644 tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php create mode 100644 tests/Feature/API/App/AuditItems/AuditItemsGetTest.php create mode 100644 tests/Unit/Models/AuditItemTest.php diff --git a/app/Console/Commands/TestCommand.php b/app/Console/Commands/TestCommand.php index 907facdb..c9f7e1ed 100644 --- a/app/Console/Commands/TestCommand.php +++ b/app/Console/Commands/TestCommand.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Models\AuditItem; use App\Models\Team; use App\Models\User; use App\Models\Voucher; @@ -33,5 +34,8 @@ public function handle() $teams = Team::factory(100)->createQuietly(); $vouchers = Voucher::factory(100)->createQuietly(); $voucherSets = VoucherSet::factory(100)->createQuietly(); + $auditItems = AuditItem::factory(100)->createQuietly([ + 'team_id' => 1 + ]); } } diff --git a/app/Enums/PersonalAccessTokenAbility.php b/app/Enums/PersonalAccessTokenAbility.php index 6c4183f1..b34b3300 100644 --- a/app/Enums/PersonalAccessTokenAbility.php +++ b/app/Enums/PersonalAccessTokenAbility.php @@ -7,17 +7,21 @@ */ enum PersonalAccessTokenAbility: string { - case SUPER_ADMIN = 'super-admin'; // Allowed to do everything - case MY_PROFILE_CREATE = 'my-profile-create'; - case MY_PROFILE_READ = 'my-profile-read'; - case MY_PROFILE_UPDATE = 'my-profile-update'; - case MY_PROFILE_DELETE = 'my-profile-delete'; - case MY_TEAM_VOUCHERS_CREATE = 'my-team-vouchers-create'; - case MY_TEAM_VOUCHERS_READ = 'my-team-vouchers-read'; - case MY_TEAM_VOUCHERS_UPDATE = 'my-team-vouchers-update'; - case MY_TEAM_VOUCHERS_DELETE = 'my-team-vouchers-delete'; - case SYSTEM_STATISTICS_CREATE = 'system-statistics-create'; - case SYSTEM_STATISTICS_READ = 'system-statistics-read'; - case SYSTEM_STATISTICS_UPDATE = 'system-statistics-update'; - case SYSTEM_STATISTICS_DELETE = 'system-statistics-delete'; + case SUPER_ADMIN = 'super-admin'; // Allowed to do everything + case MY_PROFILE_CREATE = 'my-profile-create'; + case MY_PROFILE_READ = 'my-profile-read'; + case MY_PROFILE_UPDATE = 'my-profile-update'; + case MY_PROFILE_DELETE = 'my-profile-delete'; + case MY_TEAM_VOUCHERS_CREATE = 'my-team-vouchers-create'; + case MY_TEAM_VOUCHERS_READ = 'my-team-vouchers-read'; + case MY_TEAM_VOUCHERS_UPDATE = 'my-team-vouchers-update'; + case MY_TEAM_VOUCHERS_DELETE = 'my-team-vouchers-delete'; + case MY_TEAM_AUDIT_ITEMS_CREATE = 'my-team-audit-items-create'; + case MY_TEAM_AUDIT_ITEMS_READ = 'my-team-audit-items-read'; + case MY_TEAM_AUDIT_ITEMS_UPDATE = 'my-team-audit-items-update'; + case MY_TEAM_AUDIT_ITEMS_DELETE = 'my-team-audit-items-delete'; + case SYSTEM_STATISTICS_CREATE = 'system-statistics-create'; + case SYSTEM_STATISTICS_READ = 'system-statistics-read'; + case SYSTEM_STATISTICS_UPDATE = 'system-statistics-update'; + case SYSTEM_STATISTICS_DELETE = 'system-statistics-delete'; } diff --git a/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php b/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php new file mode 100644 index 00000000..ee61a334 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php @@ -0,0 +1,101 @@ +query = AuditItem::with($this->associatedData); + $this->query = $this->updateReadQueryBasedOnUrl(); + $this->data = $this->query->paginate($this->limit); + + return $this->respond(); + } + + /** + * POST / + * + * @return JsonResponse + */ + public function store(): JsonResponse + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } + + /** + * GET /{id} + * + * @param int $id + * + * @return JsonResponse + * + * @throws DisallowedApiFieldException + */ + public function show(int $id) + { + $this->query = AuditItem::with($this->associatedData); + $this->query = $this->updateReadQueryBasedOnUrl(); + $this->data = $this->query->find($id); + + return $this->respond(); + } + + /** + * PUT /{id} + * + * @param string $id + * + * @return JsonResponse + */ + public function update(string $id) + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } + + /** + * DELETE / {id} + * + * @param string $id + * + * @return JsonResponse + */ + public function destroy(string $id) + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/ApiAdminSearchController.php b/app/Http/Controllers/Api/V1/Admin/ApiAdminSearchController.php index 07fc09ad..54b1a63b 100644 --- a/app/Http/Controllers/Api/V1/Admin/ApiAdminSearchController.php +++ b/app/Http/Controllers/Api/V1/Admin/ApiAdminSearchController.php @@ -21,9 +21,7 @@ class ApiAdminSearchController extends Controller */ public array $availableRelations = []; - public static array $searchableFields = [ - 'id', - ]; + public static array $searchableFields = []; /** * GET / diff --git a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php new file mode 100644 index 00000000..20a7e751 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php @@ -0,0 +1,98 @@ +query = AuditItem::where('team_id', Auth::user()->current_team_id)->with($this->associatedData); + $this->query = $this->updateReadQueryBasedOnUrl(); + $this->data = $this->query->paginate($this->limit); + + return $this->respond(); + } + + /** + * POST / + * + * @return JsonResponse + */ + public function store(): JsonResponse + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } + + /** + * GET /{id} + * + * @param int $id + * + * @return JsonResponse + */ + public function show(int $id) + { + $this->query = AuditItem::where('team_id', Auth::user()->current_team_id)->with($this->associatedData); + $this->query = $this->updateReadQueryBasedOnUrl(); + $this->data = $this->query->find($id); + + return $this->respond(); + } + + /** + * PUT /{id} + * + * @param string $id + * + * @return JsonResponse + */ + public function update(string $id) + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } + + /** + * DELETE / {id} + * + * @param string $id + * + * @return JsonResponse + */ + public function destroy(string $id) + { + $this->responseCode = 403; + $this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value; + + return $this->respond(); + } +} diff --git a/app/Models/AuditItem.php b/app/Models/AuditItem.php new file mode 100644 index 00000000..8dd81083 --- /dev/null +++ b/app/Models/AuditItem.php @@ -0,0 +1,24 @@ +morphTo(); + } + + public function team(): BelongsTo + { + return $this->belongsTo( Team::class, 'team_id', 'id'); + } +} diff --git a/app/Models/Team.php b/app/Models/Team.php index 3109a3cd..3a4d4e43 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; class Team extends Model @@ -16,4 +17,9 @@ public function teamUsers(): HasMany { return $this->hasMany(TeamUser::class, 'team_id', 'id'); } + + public function auditItems(): MorphMany + { + return $this->morphMany(AuditItem::class, 'auditable'); + } } diff --git a/app/Models/User.php b/app/Models/User.php index bc530580..f04efd67 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -53,4 +54,9 @@ public function teamUsers(): HasMany { return $this->hasMany(TeamUser::class); } + + public function auditItems(): MorphMany + { + return $this->morphMany(AuditItem::class, 'auditable'); + } } diff --git a/database/factories/AuditItemFactory.php b/database/factories/AuditItemFactory.php new file mode 100644 index 00000000..259a9239 --- /dev/null +++ b/database/factories/AuditItemFactory.php @@ -0,0 +1,47 @@ + + */ +class AuditItemFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $num = rand(0, 3); + + // If $num == 0, stays as user + $auditable = User::factory()->createQuietly(); + + if ($num === 1) { + $auditable = Team::factory()->createQuietly(); + } + + if ($num === 2) { + $auditable = Voucher::factory()->createQuietly(); + } + + if ($num === 3) { + $auditable = VoucherSet::factory()->createQuietly(); + } + + return [ + 'auditable_type' => get_class($auditable), + 'auditable_id' => $auditable->id, + 'auditable_text' => $this->faker->randomElement(['created', 'updated', 'deleted']), + 'team_id' => $this->faker->randomDigitNotNull(), + ]; + } +} diff --git a/database/migrations/2024_08_14_052516_create_audit_items_table.php b/database/migrations/2024_08_14_052516_create_audit_items_table.php new file mode 100644 index 00000000..78d7346d --- /dev/null +++ b/database/migrations/2024_08_14_052516_create_audit_items_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('auditable_type')->index('ai_at'); + $table->string('auditable_id'); + $table->string('auditable_text'); + $table->unsignedBigInteger('team_id')->index('ai_ti'); + $table->timestamps(); + $table->softDeletes(); + + $table->index(columns: ['auditable_type', 'auditable_id', 'auditable_text'], name: 'ai_ataiat'); + $table->index(columns: ['auditable_type', 'auditable_id'], name: 'ai_atai'); + $table->index(columns: ['auditable_text', 'team_id'], name: 'ai_atti'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('audit_items'); + } +}; diff --git a/package-lock.json b/package-lock.json index 0d0bbd1b..3bea1885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,12 @@ { - "name": "open-food-network-vouchers", + "name": "vine", "lockfileVersion": 3, "requires": true, "packages": { "": { + "dependencies": { + "moment": "^2.30.1" + }, "devDependencies": { "@inertiajs/vue3": "^1.0.0", "@tailwindcss/forms": "^0.5.3", @@ -1879,6 +1882,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", diff --git a/package.json b/package.json index 25a8081b..bdc7c93e 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "tailwindcss": "^3.2.1", "vite": "^5.0", "vue": "^3.4.0" + }, + "dependencies": { + "moment": "^2.30.1" } } diff --git a/resources/js/Components/AuditItemsComponent.vue b/resources/js/Components/AuditItemsComponent.vue new file mode 100644 index 00000000..dbbbc53a --- /dev/null +++ b/resources/js/Components/AuditItemsComponent.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/Layouts/AuthenticatedLayout.vue b/resources/js/Layouts/AuthenticatedLayout.vue index 9fdb841c..f4b53d09 100644 --- a/resources/js/Layouts/AuthenticatedLayout.vue +++ b/resources/js/Layouts/AuthenticatedLayout.vue @@ -32,6 +32,9 @@ const showingNavigationDropdown = ref(false); Dashboard + + Audit Trail + @@ -121,6 +124,9 @@ const showingNavigationDropdown = ref(false); Dashboard + + Audit Trail + diff --git a/resources/js/Pages/AuditItems.vue b/resources/js/Pages/AuditItems.vue new file mode 100644 index 00000000..f7cffd7c --- /dev/null +++ b/resources/js/Pages/AuditItems.vue @@ -0,0 +1,78 @@ + + + diff --git a/routes/api.php b/routes/api.php index 69e2db8f..8a8cabaf 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ group(function () { + /** + * My Audit Items + */ + Route::post('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'store']) + ->name('api.v1.my-team-audit-items.post') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_CREATE->value, + ] + ); + + Route::get('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'index']) + ->name('api.v1.my-team-audit-items.getMany') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); + + Route::get('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'show']) + ->name('api.v1.my-team-audit-items.get') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); + + Route::put('/my-team-audit-items/{id}', [ApiMyTeamVouchersController::class, 'update']) + ->name('api.v1.my-team-audit-items.put') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value, + ] + ); + + Route::delete('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'destroy']) + ->name('api.v1.my-team-audit-items.delete') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_DELETE->value, + ] + ); + /** * My Vouchers */ @@ -135,6 +190,7 @@ Route::prefix('admin') ->middleware(['auth:sanctum', CheckAdminStatus::class]) ->group(function () { + Route::resource('/audit-items', ApiAdminAuditItemsController::class)->names('api.v1.admin.audit-items'); Route::resource('/search', ApiAdminSearchController::class)->names('api.v1.admin.search'); Route::resource('/system-statistics', ApiAdminSystemStatisticsController::class)->names('api.v1.admin.system-statistics'); Route::resource('/team-merchant-teams', ApiAdminTeamMerchantTeamsController::class)->names('api.v1.admin.team-merchant-teams'); diff --git a/routes/web.php b/routes/web.php index 62c28e18..77b0d27b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,6 +24,10 @@ Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); + Route::get('/audit-trail', function () { + return Inertia::render('AuditItems'); + })->name('audit-trail'); + /** * Admin routes */ @@ -32,6 +36,10 @@ return Inertia::render('Admin/AdminHome'); })->name('admin.home'); + Route::get('/audit-trail', function () { + return Inertia::render('AuditItems'); + })->name('audit-trail'); + Route::get('/users', function () { return Inertia::render('Admin/Users/Users'); })->name('admin.users'); diff --git a/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPostTest.php b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPostTest.php new file mode 100644 index 00000000..0d76689a --- /dev/null +++ b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPostTest.php @@ -0,0 +1,47 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user); + + $payload = []; + + $response = $this->postJson($this->apiRoot . $this->endpoint, $payload); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotStoreAResource() + { + $this->user = $this->createAdminUser(); + + Sanctum::actingAs( + $this->user + ); + + $payload = []; + + $response = $this->postJson($this->apiRoot . $this->endpoint, $payload); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPutTest.php b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPutTest.php new file mode 100644 index 00000000..bf2a9c11 --- /dev/null +++ b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemPutTest.php @@ -0,0 +1,48 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user); + + $model = AuditItem::factory()->create(); + + $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotUpdateAResource() + { + $this->user = $this->createAdminUser(); + + Sanctum::actingAs( + $this->user + ); + + $model = AuditItem::factory()->create(); + + $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsDeleteTest.php b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsDeleteTest.php new file mode 100644 index 00000000..5fc8e789 --- /dev/null +++ b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsDeleteTest.php @@ -0,0 +1,49 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user); + + $model = AuditItem::factory()->create(); + + $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotDelete() + { + $this->user = $this->createAdminUser(); + + Sanctum::actingAs( + $this->user + ); + + $model = User::factory()->create(); + + $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsGetTest.php b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsGetTest.php new file mode 100644 index 00000000..cc9dcf90 --- /dev/null +++ b/tests/Feature/API/Admin/AdminAuditItems/AdminAuditItemsGetTest.php @@ -0,0 +1,67 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401); + } + + #[Test] + public function itCanGetAllResources() + { + $this->user = $this->createAdminUser(); + + Sanctum::actingAs( + $this->user + ); + + $rand = rand(5, 10); + + AuditItem::factory($rand)->create(); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(200); + + } + + #[Test] + public function itCanGetASingleResource() + { + $this->user = $this->createAdminUser(); + + Sanctum::actingAs( + $this->user + ); + + $model = AuditItem::factory()->create(); + + $response = $this->getJson($this->apiRoot . $this->endpoint . '/' . $model->id); + $responseObj = json_decode($response->getContent()); + + $response->assertStatus(200); + $this->assertEquals($model->id, $responseObj->data->id); + } +} diff --git a/tests/Feature/API/App/AuditItems/AuditItemPostTest.php b/tests/Feature/API/App/AuditItems/AuditItemPostTest.php new file mode 100644 index 00000000..511f3b20 --- /dev/null +++ b/tests/Feature/API/App/AuditItems/AuditItemPostTest.php @@ -0,0 +1,77 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user, abilities: []); + + $model = AuditItem::factory()->create(); + + $response = $this->postJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotCreateWithIncorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->postJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401)->assertJson( + [ + 'meta' => [ + 'message' => ApiResponse::RESPONSE_TOKEN_NOT_ALLOWED_TO_DO_THIS->value, + ], + ] + ); + } + + #[Test] + public function itCannotCreateWithCorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user, + abilities: [PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_CREATE->value] + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->postJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/App/AuditItems/AuditItemPutTest.php b/tests/Feature/API/App/AuditItems/AuditItemPutTest.php new file mode 100644 index 00000000..ea70fc27 --- /dev/null +++ b/tests/Feature/API/App/AuditItems/AuditItemPutTest.php @@ -0,0 +1,77 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user, abilities: []); + + $model = AuditItem::factory()->create(); + + $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotUpdateWithIncorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401)->assertJson( + [ + 'meta' => [ + 'message' => ApiResponse::RESPONSE_TOKEN_NOT_ALLOWED_TO_DO_THIS->value, + ], + ] + ); + } + + #[Test] + public function itCannotUpdateWithCorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user, + abilities: [PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value] + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php new file mode 100644 index 00000000..18bda17a --- /dev/null +++ b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php @@ -0,0 +1,78 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user, abilities: []); + + $model = AuditItem::factory()->create(); + + $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401); + } + + #[Test] + public function itCannotDeleteWithIncorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(401)->assertJson( + [ + 'meta' => [ + 'message' => ApiResponse::RESPONSE_TOKEN_NOT_ALLOWED_TO_DO_THIS->value, + ], + ] + ); + } + + #[Test] + public function itCannotDeleteWithCorrectToken() + { + $this->user = $this->createUserWithTeam(); + + Sanctum::actingAs( + $this->user, + abilities: [PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_DELETE->value] + ); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->user->current_team_id, + ]); + + $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php new file mode 100644 index 00000000..5fd1412e --- /dev/null +++ b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php @@ -0,0 +1,149 @@ +user = $this->createUser(); + + Sanctum::actingAs($this->user, abilities: []); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401); + } + + #[Test] + public function standardUserWithoutPermissionCannotAccess() + { + $this->user = $this->createUserWithTeam(); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->team->id, + ]); + + Sanctum::actingAs($this->user, abilities: []); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401)->assertJson( + [ + 'meta' => [ + 'message' => ApiResponse::RESPONSE_TOKEN_NOT_ALLOWED_TO_DO_THIS->value, + ], + ] + ); + + } + + #[Test] + public function itCanGetAllResources() + { + $this->user = $this->createUserWithTeam(); + + $num = rand(1, 40); + + AuditItem::factory($num)->create([ + 'team_id' => $this->team->id, + ]); + + // Different team, should be inaccessible + AuditItem::factory($num)->create([ + 'team_id' => $this->team->id + 1, + ]); + + Sanctum::actingAs($this->user, abilities: [ + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ]); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(200); + + $responseObject = json_decode($response->getContent(), false); + + self::assertCount($num, $responseObject->data->data); + + foreach ($responseObject->data->data as $auditItem) { + self::assertSame($this->user->current_team_id, $auditItem->team_id); + } + } + + #[Test] + public function itCanNotGetAllResourcesIncorrectAbility() + { + $this->user = $this->createUserWithTeam(); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->team->id, + ]); + + Sanctum::actingAs($this->user, abilities: [ + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value, + ]); + + $response = $this->getJson($this->apiRoot . $this->endpoint); + + $response->assertStatus(401)->assertJson( + [ + 'meta' => [ + 'message' => ApiResponse::RESPONSE_TOKEN_NOT_ALLOWED_TO_DO_THIS->value, + ], + ] + ); + + } + + #[Test] + public function itCanGetASingleResource() + { + $this->user = $this->createUserWithTeam(); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->team->id, + ]); + + Sanctum::actingAs($this->user, abilities: [ + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ]); + + $response = $this->getJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(200); + } + + #[Test] + public function itCanNotGetASingleResourceFromAnotherTeam() + { + $this->user = $this->createUserWithTeam(); + + $model = AuditItem::factory()->create([ + 'team_id' => $this->team->id + 1, + ]); + + Sanctum::actingAs($this->user, abilities: [ + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ]); + + $response = $this->getJson($this->apiRoot . $this->endpoint . '/' . $model->id); + + $response->assertStatus(404); + } +} diff --git a/tests/Unit/Models/AuditItemTest.php b/tests/Unit/Models/AuditItemTest.php new file mode 100644 index 00000000..7d3a88bf --- /dev/null +++ b/tests/Unit/Models/AuditItemTest.php @@ -0,0 +1,86 @@ +create(); + + $auditItem = AuditItem::factory()->create([ + 'auditable_type' => get_class($user), + 'auditable_id' => $user->id, + ]); + + $auditItemWithUser = AuditItem::find($auditItem->id); + + $this->assertInstanceOf(AuditItem::class, $auditItemWithUser); + $this->assertInstanceOf(User::class, $auditItemWithUser->auditable); + $this->assertSame(User::class, $auditItemWithUser->auditable_type); + $this->assertTrue($auditItemWithUser->auditable->is($user)); + } + + #[Test] + public function testTeamRelation(): void + { + $team = Team::factory()->create(); + + $auditItem = AuditItem::factory()->create([ + 'auditable_type' => get_class($team), + 'auditable_id' => $team->id, + ]); + + $auditItemWithTeam = AuditItem::find($auditItem->id); + + $this->assertInstanceOf(AuditItem::class, $auditItemWithTeam); + $this->assertInstanceOf(Team::class, $auditItemWithTeam->auditable); + $this->assertSame(Team::class, $auditItemWithTeam->auditable_type); + $this->assertTrue($auditItemWithTeam->auditable->is($team)); + } + + #[Test] + public function testVoucherRelation(): void + { + $voucher = Voucher::factory()->create(); + + $auditItem = AuditItem::factory()->create([ + 'auditable_type' => get_class($voucher), + 'auditable_id' => $voucher->id, + ]); + + $auditItemWithVoucher = AuditItem::find($auditItem->id); + + $this->assertInstanceOf(AuditItem::class, $auditItemWithVoucher); + $this->assertInstanceOf(Voucher::class, $auditItemWithVoucher->auditable); + $this->assertSame(Voucher::class, $auditItemWithVoucher->auditable_type); + $this->assertTrue($auditItemWithVoucher->auditable->is($voucher)); + } + + #[Test] + public function testVoucherSetRelation(): void + { + $voucherSet = VoucherSet::factory()->create(); + + $auditItem = AuditItem::factory()->create([ + 'auditable_type' => get_class($voucherSet), + 'auditable_id' => $voucherSet->id, + ]); + + $auditItemWithVoucherSet = AuditItem::find($auditItem->id); + + $this->assertInstanceOf(AuditItem::class, $auditItemWithVoucherSet); + $this->assertInstanceOf(VoucherSet::class, $auditItemWithVoucherSet->auditable); + $this->assertSame(VoucherSet::class, $auditItemWithVoucherSet->auditable_type); + $this->assertTrue($auditItemWithVoucherSet->auditable->is($voucherSet)); + } +} diff --git a/tests/Unit/Models/TeamTest.php b/tests/Unit/Models/TeamTest.php index 9bd75fa2..f273b116 100644 --- a/tests/Unit/Models/TeamTest.php +++ b/tests/Unit/Models/TeamTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Models; +use App\Models\AuditItem; use App\Models\Team; use App\Models\TeamUser; use App\Models\User; @@ -64,4 +65,39 @@ public function testUserRelation(): void $this->assertTrue($users->contains($teamUser->user)); }); } + + #[Test] + public function testAuditItemsRelation(): void + { + $team = Team::factory()->create(); + + $num = rand(1, 10); + + AuditItem::factory($num)->create([ + 'auditable_type' => Team::class, + 'auditable_id' => $team->id, + ]); + + // Same ID but different class, should NOT be returned + AuditItem::factory($num)->create([ + 'auditable_type' => User::class, + 'auditable_id' => $team->id, + ]); + + // Same class but different ID, should NOT be returned + AuditItem::factory($num)->create([ + 'auditable_type' => Team::class, + 'auditable_id' => $team->id + 1, + ]); + + $teamWithAuditItems = Team::with('auditItems')->find($team->id); + + $this->assertInstanceOf(Team::class, $teamWithAuditItems); + $this->assertCount($num, $teamWithAuditItems->auditItems); + $teamWithAuditItems->auditItems->map(function ($auditItem) use ($team) { + $this->assertEquals($team->id, $auditItem->auditable_id); + $this->assertSame(Team::class, $auditItem->auditable_type); + $this->assertInstanceOf(AuditItem::class, $auditItem); + }); + } } diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 53f0757c..33989b32 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Models; +use App\Models\AuditItem; use App\Models\Team; use App\Models\TeamUser; use App\Models\User; @@ -29,4 +30,39 @@ public function testTeamUserRelation(): void $this->assertSame($teamUser->user_id, $userWithTeamUsers->teamUsers->first()->user_id); $this->assertSame($teamUser->team_id, $userWithTeamUsers->teamUsers->first()->team_id); } + + #[Test] + public function testAuditItemsRelation(): void + { + $user = User::factory()->create(); + + $num = rand(1, 10); + + AuditItem::factory($num)->create([ + 'auditable_type' => User::class, + 'auditable_id' => $user->id, + ]); + + // Same ID but different class, should NOT be returned + AuditItem::factory($num)->create([ + 'auditable_type' => Team::class, + 'auditable_id' => $user->id, + ]); + + // Same class but different ID, should NOT be returned + AuditItem::factory($num)->create([ + 'auditable_type' => User::class, + 'auditable_id' => $user->id + 1, + ]); + + $userWithAuditItems = User::with('auditItems')->find($user->id); + + $this->assertInstanceOf(User::class, $userWithAuditItems); + $this->assertCount($num, $userWithAuditItems->auditItems); + $userWithAuditItems->auditItems->map(function ($auditItem) use ($user) { + $this->assertEquals($user->id, $auditItem->auditable_id); + $this->assertSame(User::class, $auditItem->auditable_type); + $this->assertInstanceOf(AuditItem::class, $auditItem); + }); + } } From 4caf53898dbb6711c7634f703fe43100cf346bcd Mon Sep 17 00:00:00 2001 From: ok200lyndon Date: Thu, 15 Aug 2024 06:54:02 +0000 Subject: [PATCH 02/15] Lint code --- app/Console/Commands/TestCommand.php | 2 +- .../Api/V1/Admin/ApiAdminAuditItemsController.php | 2 +- app/Models/AuditItem.php | 3 +-- .../2024_08_14_052516_create_audit_items_table.php | 2 +- tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php | 1 - tests/Unit/Models/UserTest.php | 6 +++--- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/Console/Commands/TestCommand.php b/app/Console/Commands/TestCommand.php index c9f7e1ed..35b8e1c8 100644 --- a/app/Console/Commands/TestCommand.php +++ b/app/Console/Commands/TestCommand.php @@ -35,7 +35,7 @@ public function handle() $vouchers = Voucher::factory(100)->createQuietly(); $voucherSets = VoucherSet::factory(100)->createQuietly(); $auditItems = AuditItem::factory(100)->createQuietly([ - 'team_id' => 1 + 'team_id' => 1, ]); } } diff --git a/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php b/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php index ee61a334..1f50cdfc 100644 --- a/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php +++ b/app/Http/Controllers/Api/V1/Admin/ApiAdminAuditItemsController.php @@ -17,7 +17,7 @@ class ApiAdminAuditItemsController extends Controller * Set the related data the GET request is allowed to ask for */ public array $availableRelations = [ - 'team' + 'team', ]; public static array $searchableFields = []; diff --git a/app/Models/AuditItem.php b/app/Models/AuditItem.php index 8dd81083..5c92d289 100644 --- a/app/Models/AuditItem.php +++ b/app/Models/AuditItem.php @@ -5,7 +5,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; class AuditItem extends Model @@ -19,6 +18,6 @@ public function auditable(): MorphTo public function team(): BelongsTo { - return $this->belongsTo( Team::class, 'team_id', 'id'); + return $this->belongsTo(Team::class, 'team_id', 'id'); } } diff --git a/database/migrations/2024_08_14_052516_create_audit_items_table.php b/database/migrations/2024_08_14_052516_create_audit_items_table.php index 78d7346d..7b5d9fed 100644 --- a/database/migrations/2024_08_14_052516_create_audit_items_table.php +++ b/database/migrations/2024_08_14_052516_create_audit_items_table.php @@ -4,7 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration +return new class() extends Migration { /** * Run the migrations. diff --git a/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php index 18bda17a..f1bbadbd 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php @@ -5,7 +5,6 @@ use App\Enums\ApiResponse; use App\Enums\PersonalAccessTokenAbility; use App\Models\AuditItem; -use App\Models\User; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\WithFaker; use Laravel\Sanctum\Sanctum; diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 33989b32..f972405b 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -40,19 +40,19 @@ public function testAuditItemsRelation(): void AuditItem::factory($num)->create([ 'auditable_type' => User::class, - 'auditable_id' => $user->id, + 'auditable_id' => $user->id, ]); // Same ID but different class, should NOT be returned AuditItem::factory($num)->create([ 'auditable_type' => Team::class, - 'auditable_id' => $user->id, + 'auditable_id' => $user->id, ]); // Same class but different ID, should NOT be returned AuditItem::factory($num)->create([ 'auditable_type' => User::class, - 'auditable_id' => $user->id + 1, + 'auditable_id' => $user->id + 1, ]); $userWithAuditItems = User::with('auditItems')->find($user->id); From 6ba74295c23e3c89b345f2f127be20fd60fea968 Mon Sep 17 00:00:00 2001 From: ok200lyndon Date: Fri, 16 Aug 2024 16:24:27 +1000 Subject: [PATCH 03/15] refactor and update tests --- app/Events/Users/UserWasCreated.php | 37 +++++++++++++ .../Api/V1/ApiMyTeamAuditItemsController.php | 11 ++-- app/Jobs/RecordUserWasCreatedAuditItem.php | 41 ++++++++++++++ .../Users/HandleUserWasCreatedEvent.php | 33 +++++++++++ app/Models/AuditItem.php | 8 +++ app/Models/User.php | 5 ++ app/Providers/AppServiceProvider.php | 11 +++- app/Services/AuditItemService.php | 33 +++++++++++ database/factories/AuditItemFactory.php | 32 +++-------- ..._08_14_052516_create_audit_items_table.php | 5 +- database/seeders/DatabaseSeeder.php | 2 +- routes/web.php | 2 +- .../API/App/AuditItems/AuditItemPostTest.php | 4 +- .../API/App/AuditItems/AuditItemPutTest.php | 4 +- .../App/AuditItems/AuditItemsDeleteTest.php | 4 +- .../API/App/AuditItems/AuditItemsGetTest.php | 14 ++--- tests/Unit/Services/AuditItemServiceTest.php | 55 +++++++++++++++++++ 17 files changed, 253 insertions(+), 48 deletions(-) create mode 100644 app/Events/Users/UserWasCreated.php create mode 100644 app/Jobs/RecordUserWasCreatedAuditItem.php create mode 100644 app/Listeners/Users/HandleUserWasCreatedEvent.php create mode 100644 app/Services/AuditItemService.php create mode 100644 tests/Unit/Services/AuditItemServiceTest.php diff --git a/app/Events/Users/UserWasCreated.php b/app/Events/Users/UserWasCreated.php new file mode 100644 index 00000000..fd9e8c99 --- /dev/null +++ b/app/Events/Users/UserWasCreated.php @@ -0,0 +1,37 @@ + + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php index 20a7e751..a41eb80f 100644 --- a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php +++ b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php @@ -30,7 +30,7 @@ class ApiMyTeamAuditItemsController extends Controller */ public function index(): JsonResponse { - $this->query = AuditItem::where('team_id', Auth::user()->current_team_id)->with($this->associatedData); + $this->query = AuditItem::where('auditable_team_id', Auth::user()->current_team_id)->with($this->associatedData); $this->query = $this->updateReadQueryBasedOnUrl(); $this->data = $this->query->paginate($this->limit); @@ -53,13 +53,14 @@ public function store(): JsonResponse /** * GET /{id} * - * @param int $id + * @param int $id * * @return JsonResponse + * @throws DisallowedApiFieldException */ public function show(int $id) { - $this->query = AuditItem::where('team_id', Auth::user()->current_team_id)->with($this->associatedData); + $this->query = AuditItem::where('auditable_team_id', Auth::user()->current_team_id)->with($this->associatedData); $this->query = $this->updateReadQueryBasedOnUrl(); $this->data = $this->query->find($id); @@ -69,7 +70,7 @@ public function show(int $id) /** * PUT /{id} * - * @param string $id + * @param string $id * * @return JsonResponse */ @@ -84,7 +85,7 @@ public function update(string $id) /** * DELETE / {id} * - * @param string $id + * @param string $id * * @return JsonResponse */ diff --git a/app/Jobs/RecordUserWasCreatedAuditItem.php b/app/Jobs/RecordUserWasCreatedAuditItem.php new file mode 100644 index 00000000..8ce75efc --- /dev/null +++ b/app/Jobs/RecordUserWasCreatedAuditItem.php @@ -0,0 +1,41 @@ +actioningUser->is_admin) { + $eventText .= 'Admin '; + } + + $eventText .= $this->actioningUser->name . ' created a new user ' . $this->createdUser->name . '.'; + + AuditItemService::createAuditItemForEvent( + actioningUser: $this->actioningUser, + model: $this->createdUser, + eventText: $eventText + ); + } +} diff --git a/app/Listeners/Users/HandleUserWasCreatedEvent.php b/app/Listeners/Users/HandleUserWasCreatedEvent.php new file mode 100644 index 00000000..76762960 --- /dev/null +++ b/app/Listeners/Users/HandleUserWasCreatedEvent.php @@ -0,0 +1,33 @@ +user + ) + ); + } + } +} diff --git a/app/Models/AuditItem.php b/app/Models/AuditItem.php index 5c92d289..ebc66a8d 100644 --- a/app/Models/AuditItem.php +++ b/app/Models/AuditItem.php @@ -11,6 +11,14 @@ class AuditItem extends Model { use HasFactory; + protected $fillable = [ + 'auditable_id', + 'auditable_type', + 'auditable_text', + 'team_id', + 'user_id', + ]; + public function auditable(): MorphTo { return $this->morphTo(); diff --git a/app/Models/User.php b/app/Models/User.php index f04efd67..f6a9580f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Events\Users\UserWasCreated; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; @@ -16,6 +17,10 @@ class User extends Authenticatable use HasFactory; use Notifiable; + protected $dispatchesEvents = [ + 'created' => UserWasCreated::class, + ]; + /** * The attributes that are mass assignable. * diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8060b78d..5b352afe 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Events\Users\UserWasCreated; +use App\Listeners\Users\HandleUserWasCreatedEvent; +use Event; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -14,5 +17,11 @@ public function register(): void {} /** * Bootstrap any application services. */ - public function boot(): void {} + public function boot(): void + { + Event::listen( + events: UserWasCreated::class, + listener: HandleUserWasCreatedEvent::class + ); + } } diff --git a/app/Services/AuditItemService.php b/app/Services/AuditItemService.php new file mode 100644 index 00000000..3d45c045 --- /dev/null +++ b/app/Services/AuditItemService.php @@ -0,0 +1,33 @@ +hasAttribute('team_id')) { + $teamId = $model->team_id; + } + + if ($model->hasAttribute('current_team_id')) { + $teamId = $model->current_team_id; + } + + $auditItem = new AuditItem(); + $auditItem->auditable_type = get_class($model); + $auditItem->auditable_id = $model->id; + $auditItem->auditable_text = $eventText; + $auditItem->auditable_team_id = $teamId; + $auditItem->actioning_user_id = $actioningUser->id; + $auditItem->save(); + + return $auditItem; + } +} diff --git a/database/factories/AuditItemFactory.php b/database/factories/AuditItemFactory.php index 259a9239..5fb52784 100644 --- a/database/factories/AuditItemFactory.php +++ b/database/factories/AuditItemFactory.php @@ -2,14 +2,12 @@ namespace Database\Factories; -use App\Models\Team; +use App\Models\AuditItem; use App\Models\User; -use App\Models\Voucher; -use App\Models\VoucherSet; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuditItem> + * @extends Factory */ class AuditItemFactory extends Factory { @@ -20,28 +18,12 @@ class AuditItemFactory extends Factory */ public function definition(): array { - $num = rand(0, 3); - - // If $num == 0, stays as user - $auditable = User::factory()->createQuietly(); - - if ($num === 1) { - $auditable = Team::factory()->createQuietly(); - } - - if ($num === 2) { - $auditable = Voucher::factory()->createQuietly(); - } - - if ($num === 3) { - $auditable = VoucherSet::factory()->createQuietly(); - } - return [ - 'auditable_type' => get_class($auditable), - 'auditable_id' => $auditable->id, - 'auditable_text' => $this->faker->randomElement(['created', 'updated', 'deleted']), - 'team_id' => $this->faker->randomDigitNotNull(), + 'auditable_type' => User::class, + 'auditable_id' => $this->faker->randomDigitNotNull(), + 'auditable_text' => $this->faker->randomElement(['created', 'updated', 'deleted']), + 'auditable_team_id' => $this->faker->randomDigitNotNull(), + 'actioning_user_id' => $this->faker->randomDigitNotNull(), ]; } } diff --git a/database/migrations/2024_08_14_052516_create_audit_items_table.php b/database/migrations/2024_08_14_052516_create_audit_items_table.php index 7b5d9fed..371ad537 100644 --- a/database/migrations/2024_08_14_052516_create_audit_items_table.php +++ b/database/migrations/2024_08_14_052516_create_audit_items_table.php @@ -16,13 +16,14 @@ public function up(): void $table->string('auditable_type')->index('ai_at'); $table->string('auditable_id'); $table->string('auditable_text'); - $table->unsignedBigInteger('team_id')->index('ai_ti'); + $table->unsignedBigInteger('auditable_team_id')->nullable()->index('ai_ati'); + $table->unsignedBigInteger('actioning_user_id')->index('ai_aui'); $table->timestamps(); $table->softDeletes(); $table->index(columns: ['auditable_type', 'auditable_id', 'auditable_text'], name: 'ai_ataiat'); $table->index(columns: ['auditable_type', 'auditable_id'], name: 'ai_atai'); - $table->index(columns: ['auditable_text', 'team_id'], name: 'ai_atti'); + $table->index(columns: ['auditable_text', 'auditable_team_id'], name: 'ai_atati'); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 667b70a7..5fe6126b 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -64,7 +64,7 @@ public function run(): void ); foreach ($userAndTeam['users'] as $user) { - $user = User::factory()->create( + $user = User::factory()->createQuietly( [ 'name' => $user['name'], 'email' => $user['email'], diff --git a/routes/web.php b/routes/web.php index 77b0d27b..a4b8fbff 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,7 +38,7 @@ Route::get('/audit-trail', function () { return Inertia::render('AuditItems'); - })->name('audit-trail'); + })->name('admin.audit-trail'); Route::get('/users', function () { return Inertia::render('Admin/Users/Users'); diff --git a/tests/Feature/API/App/AuditItems/AuditItemPostTest.php b/tests/Feature/API/App/AuditItems/AuditItemPostTest.php index 511f3b20..7969235e 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemPostTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemPostTest.php @@ -42,7 +42,7 @@ public function itCannotCreateWithIncorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->postJson($this->apiRoot . $this->endpoint); @@ -67,7 +67,7 @@ public function itCannotCreateWithCorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->postJson($this->apiRoot . $this->endpoint); diff --git a/tests/Feature/API/App/AuditItems/AuditItemPutTest.php b/tests/Feature/API/App/AuditItems/AuditItemPutTest.php index ea70fc27..c7c33c12 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemPutTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemPutTest.php @@ -42,7 +42,7 @@ public function itCannotUpdateWithIncorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); @@ -67,7 +67,7 @@ public function itCannotUpdateWithCorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->putJson($this->apiRoot . $this->endpoint . '/' . $model->id); diff --git a/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php index f1bbadbd..96238838 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemsDeleteTest.php @@ -42,7 +42,7 @@ public function itCannotDeleteWithIncorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); @@ -67,7 +67,7 @@ public function itCannotDeleteWithCorrectToken() ); $model = AuditItem::factory()->create([ - 'team_id' => $this->user->current_team_id, + 'auditable_team_id' => $this->user->current_team_id, ]); $response = $this->deleteJson($this->apiRoot . $this->endpoint . '/' . $model->id); diff --git a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php index 5fd1412e..82f3f3a2 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php @@ -36,7 +36,7 @@ public function standardUserWithoutPermissionCannotAccess() $this->user = $this->createUserWithTeam(); $model = AuditItem::factory()->create([ - 'team_id' => $this->team->id, + 'auditable_team_id' => $this->team->id, ]); Sanctum::actingAs($this->user, abilities: []); @@ -61,12 +61,12 @@ public function itCanGetAllResources() $num = rand(1, 40); AuditItem::factory($num)->create([ - 'team_id' => $this->team->id, + 'auditable_team_id' => $this->team->id, ]); // Different team, should be inaccessible AuditItem::factory($num)->create([ - 'team_id' => $this->team->id + 1, + 'auditable_team_id' => $this->team->id + 1, ]); Sanctum::actingAs($this->user, abilities: [ @@ -82,7 +82,7 @@ public function itCanGetAllResources() self::assertCount($num, $responseObject->data->data); foreach ($responseObject->data->data as $auditItem) { - self::assertSame($this->user->current_team_id, $auditItem->team_id); + self::assertSame($this->user->current_team_id, $auditItem->auditable_team_id); } } @@ -92,7 +92,7 @@ public function itCanNotGetAllResourcesIncorrectAbility() $this->user = $this->createUserWithTeam(); $model = AuditItem::factory()->create([ - 'team_id' => $this->team->id, + 'auditable_team_id' => $this->team->id, ]); Sanctum::actingAs($this->user, abilities: [ @@ -117,7 +117,7 @@ public function itCanGetASingleResource() $this->user = $this->createUserWithTeam(); $model = AuditItem::factory()->create([ - 'team_id' => $this->team->id, + 'auditable_team_id' => $this->team->id, ]); Sanctum::actingAs($this->user, abilities: [ @@ -135,7 +135,7 @@ public function itCanNotGetASingleResourceFromAnotherTeam() $this->user = $this->createUserWithTeam(); $model = AuditItem::factory()->create([ - 'team_id' => $this->team->id + 1, + 'auditable_team_id' => $this->team->id + 1, ]); Sanctum::actingAs($this->user, abilities: [ diff --git a/tests/Unit/Services/AuditItemServiceTest.php b/tests/Unit/Services/AuditItemServiceTest.php new file mode 100644 index 00000000..1a592235 --- /dev/null +++ b/tests/Unit/Services/AuditItemServiceTest.php @@ -0,0 +1,55 @@ +createQuietly(); + + $num = rand(0, 3); + + // If $num == 0, stays as user + $auditableModel = User::factory()->createQuietly(); + + if ($num === 1) { + $auditableModel = Team::factory()->createQuietly(); + } + + if ($num === 2) { + $auditableModel = Voucher::factory()->createQuietly(); + } + + if ($num === 3) { + $auditableModel = VoucherSet::factory()->createQuietly(); + } + + $eventText = Str::random($num * $num); + + $auditItem = AuditItemService::createAuditItemForEvent( + actioningUser: $actioningUser, + model: $auditableModel, + eventText: $eventText, + ); + + self::assertInstanceOf(AuditItem::class, $auditItem); + self::assertSame(get_class($auditableModel), $auditItem->auditable_type); + self::assertSame($auditableModel->id, $auditItem->auditable_id); + self::assertSame($eventText, $auditItem->auditable_text); + } +} From d47c0593357244ca614217832d13c716da06db9d Mon Sep 17 00:00:00 2001 From: ok200lyndon Date: Fri, 16 Aug 2024 06:25:13 +0000 Subject: [PATCH 04/15] Lint code --- .../Controllers/Api/V1/ApiMyTeamAuditItemsController.php | 7 ++++--- app/Listeners/Users/HandleUserWasCreatedEvent.php | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php index a41eb80f..de970deb 100644 --- a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php +++ b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php @@ -53,9 +53,10 @@ public function store(): JsonResponse /** * GET /{id} * - * @param int $id + * @param int $id * * @return JsonResponse + * * @throws DisallowedApiFieldException */ public function show(int $id) @@ -70,7 +71,7 @@ public function show(int $id) /** * PUT /{id} * - * @param string $id + * @param string $id * * @return JsonResponse */ @@ -85,7 +86,7 @@ public function update(string $id) /** * DELETE / {id} * - * @param string $id + * @param string $id * * @return JsonResponse */ diff --git a/app/Listeners/Users/HandleUserWasCreatedEvent.php b/app/Listeners/Users/HandleUserWasCreatedEvent.php index 76762960..c0dfdfa0 100644 --- a/app/Listeners/Users/HandleUserWasCreatedEvent.php +++ b/app/Listeners/Users/HandleUserWasCreatedEvent.php @@ -5,7 +5,6 @@ use App\Events\Users\UserWasCreated; use App\Jobs\RecordUserWasCreatedAuditItem; use Auth; -use Log; class HandleUserWasCreatedEvent { From 6a0d38d299e6ec8265e72ad508484febf3b31e4c Mon Sep 17 00:00:00 2001 From: ok200lyndon Date: Fri, 16 Aug 2024 16:46:15 +1000 Subject: [PATCH 05/15] rework front end --- .../Api/V1/ApiMyTeamAuditItemsController.php | 4 +- app/Models/AuditItem.php | 6 +-- .../js/Components/AuditItemsComponent.vue | 41 +++++++++++-------- resources/js/Pages/AuditItems.vue | 14 ------- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php index de970deb..5ddd67d1 100644 --- a/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php +++ b/app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php @@ -17,7 +17,9 @@ class ApiMyTeamAuditItemsController extends Controller /** * Set the related data the GET request is allowed to ask for */ - public array $availableRelations = []; + public array $availableRelations = [ + 'team', + ]; public static array $searchableFields = []; diff --git a/app/Models/AuditItem.php b/app/Models/AuditItem.php index ebc66a8d..a8f095d2 100644 --- a/app/Models/AuditItem.php +++ b/app/Models/AuditItem.php @@ -15,8 +15,8 @@ class AuditItem extends Model 'auditable_id', 'auditable_type', 'auditable_text', - 'team_id', - 'user_id', + 'auditable_team_id', + 'actioning_user_id', ]; public function auditable(): MorphTo @@ -26,6 +26,6 @@ public function auditable(): MorphTo public function team(): BelongsTo { - return $this->belongsTo(Team::class, 'team_id', 'id'); + return $this->belongsTo(Team::class, 'auditable_team_id', 'id'); } } diff --git a/resources/js/Components/AuditItemsComponent.vue b/resources/js/Components/AuditItemsComponent.vue index dbbbc53a..bcc40cbe 100644 --- a/resources/js/Components/AuditItemsComponent.vue +++ b/resources/js/Components/AuditItemsComponent.vue @@ -9,32 +9,37 @@ const $props = defineProps(['auditItems', 'isAdmin'])
-
- +
+ + {{ auditItem.auditable_text }} + + + {{ auditItem.auditable_type.substring(auditItem.auditable_type.lastIndexOf("\\") + 1) }} - - + + + # {{ auditItem.auditable_id }} + + + + {{ auditItem.team.name }} - - - #{{ auditItem.auditable_id }} -
+
- - {{ auditItem.auditable_text }} - - on {{ moment(auditItem.updated_at).format("dddd, MMMM Do YYYY [at] h:mm:ss a") }} + + + {{ moment(auditItem.updated_at).format("dddd, MMMM Do YYYY [at] h:mm:ss a") }}
diff --git a/resources/js/Pages/AuditItems.vue b/resources/js/Pages/AuditItems.vue index f7cffd7c..ea16eccd 100644 --- a/resources/js/Pages/AuditItems.vue +++ b/resources/js/Pages/AuditItems.vue @@ -55,20 +55,6 @@ onMounted(() => {
-
-
- Filter to: -
-
-
User
-
Team
-
Voucher Set
-
Voucher
-
No Filter
-
-
- -
From 982bcf5628290a5e64d21181033a88b7aae86348 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:31:46 +1000 Subject: [PATCH 06/15] Update api.php --- routes/api.php | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/routes/api.php b/routes/api.php index 24d5d589..34ba09a5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ names('api.v1.my-team'); + /** + * My Audit Items + */ + Route::post('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'store']) + ->name('api.v1.my-team-audit-items.post') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_CREATE->value, + ] + ); + + Route::get('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'index']) + ->name('api.v1.my-team-audit-items.getMany') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); + + Route::get('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'show']) + ->name('api.v1.my-team-audit-items.get') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); + + Route::put('/my-team-audit-items/{id}', [ApiMyTeamVouchersController::class, 'update']) + ->name('api.v1.my-team-audit-items.put') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value, + ] + ); + + Route::delete('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'destroy']) + ->name('api.v1.my-team-audit-items.delete') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_DELETE->value, + ] + ); + /** * My Teams */ @@ -192,6 +247,7 @@ Route::prefix('admin') ->middleware(['auth:sanctum', CheckAdminStatus::class]) ->group(function () { + Route::resource('/audit-items', ApiAdminAuditItemsController::class)->names('api.v1.admin.audit-items'); Route::resource('/search', ApiAdminSearchController::class)->names('api.v1.admin.search'); Route::resource('/system-statistics', ApiAdminSystemStatisticsController::class)->names('api.v1.admin.system-statistics'); Route::resource('/team-merchant-teams', ApiAdminTeamMerchantTeamsController::class)->names('api.v1.admin.team-merchant-teams'); From bc696f081914d2a141add7c66f076fbe170c4d97 Mon Sep 17 00:00:00 2001 From: ok200paul Date: Fri, 16 Aug 2024 07:32:43 +0000 Subject: [PATCH 07/15] Lint code --- app/Enums/PersonalAccessTokenAbility.php | 2 - routes/api.php | 80 ++++++++++++------------ routes/web.php | 2 - 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/app/Enums/PersonalAccessTokenAbility.php b/app/Enums/PersonalAccessTokenAbility.php index 8255dfb5..6e864faf 100644 --- a/app/Enums/PersonalAccessTokenAbility.php +++ b/app/Enums/PersonalAccessTokenAbility.php @@ -7,7 +7,6 @@ */ enum PersonalAccessTokenAbility: string { - case SUPER_ADMIN = 'super-admin'; // Allowed to do everything case MY_PROFILE_CREATE = 'my-profile-create'; case MY_PROFILE_READ = 'my-profile-read'; @@ -29,5 +28,4 @@ enum PersonalAccessTokenAbility: string case SYSTEM_STATISTICS_READ = 'system-statistics-read'; case SYSTEM_STATISTICS_UPDATE = 'system-statistics-update'; case SYSTEM_STATISTICS_DELETE = 'system-statistics-delete'; - } diff --git a/routes/api.php b/routes/api.php index 34ba09a5..5820b46d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -32,54 +32,54 @@ * My Audit Items */ Route::post('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'store']) - ->name('api.v1.my-team-audit-items.post') - ->middleware( - [ - 'abilities:' . - PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . - PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_CREATE->value, - ] - ); + ->name('api.v1.my-team-audit-items.post') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_CREATE->value, + ] + ); Route::get('/my-team-audit-items', [ApiMyTeamAuditItemsController::class, 'index']) - ->name('api.v1.my-team-audit-items.getMany') - ->middleware( - [ - 'abilities:' . - PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . - PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, - ] - ); + ->name('api.v1.my-team-audit-items.getMany') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); Route::get('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'show']) - ->name('api.v1.my-team-audit-items.get') - ->middleware( - [ - 'abilities:' . - PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . - PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, - ] - ); + ->name('api.v1.my-team-audit-items.get') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_READ->value, + ] + ); Route::put('/my-team-audit-items/{id}', [ApiMyTeamVouchersController::class, 'update']) - ->name('api.v1.my-team-audit-items.put') - ->middleware( - [ - 'abilities:' . - PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . - PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value, - ] - ); + ->name('api.v1.my-team-audit-items.put') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_UPDATE->value, + ] + ); Route::delete('/my-team-audit-items/{id}', [ApiMyTeamAuditItemsController::class, 'destroy']) - ->name('api.v1.my-team-audit-items.delete') - ->middleware( - [ - 'abilities:' . - PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . - PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_DELETE->value, - ] - ); + ->name('api.v1.my-team-audit-items.delete') + ->middleware( + [ + 'abilities:' . + PersonalAccessTokenAbility::SUPER_ADMIN->value . ',' . + PersonalAccessTokenAbility::MY_TEAM_AUDIT_ITEMS_DELETE->value, + ] + ); /** * My Teams diff --git a/routes/web.php b/routes/web.php index 19f638b6..63d65bd4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -30,7 +30,6 @@ Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - Route::get('/audit-trail', function () { return Inertia::render('AuditItems'); })->name('audit-trail'); @@ -50,7 +49,6 @@ })->name('switch-team'); - /** * Admin routes */ From 03a6f8db511ccb84c1a598a9208365dba083cc18 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:44:37 +1000 Subject: [PATCH 08/15] Removed actioning user from audit trail --- app/Http/Middleware/HandleInertiaRequests.php | 2 +- app/Jobs/RecordUserWasCreatedAuditItem.php | 18 ++++++------------ .../Users/HandleUserWasCreatedEvent.php | 19 ++++++++++--------- app/Models/AuditItem.php | 1 - app/Services/AuditItemService.php | 12 +----------- database/factories/AuditItemFactory.php | 1 - ..._08_14_052516_create_audit_items_table.php | 1 - .../API/App/AuditItems/AuditItemsGetTest.php | 2 +- tests/Unit/Models/UserTest.php | 2 +- tests/Unit/Services/AuditItemServiceTest.php | 1 - 10 files changed, 20 insertions(+), 39 deletions(-) diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 55897b65..0d7055d3 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -39,7 +39,7 @@ public function share(Request $request): array ...parent::share($request), 'auth' => [ 'user' => $request->user(), - 'currentTeam' => Team::find($request->user()->current_team_id), + 'currentTeam' => Team::find($request->user()?->current_team_id), ], 'personalAccessTokenAbilities' => PersonalAccessTokenAbility::cases(), ]; diff --git a/app/Jobs/RecordUserWasCreatedAuditItem.php b/app/Jobs/RecordUserWasCreatedAuditItem.php index 8ce75efc..f93281c0 100644 --- a/app/Jobs/RecordUserWasCreatedAuditItem.php +++ b/app/Jobs/RecordUserWasCreatedAuditItem.php @@ -17,25 +17,19 @@ class RecordUserWasCreatedAuditItem implements ShouldQueue * @param User $actioningUser * @param User $createdUser */ - public function __construct(public User $actioningUser, public User $createdUser) {} + public function __construct(public User $createdUser) + { + } /** * Execute the job. */ public function handle(): void { - $eventText = ''; - - if ($this->actioningUser->is_admin) { - $eventText .= 'Admin '; - } - - $eventText .= $this->actioningUser->name . ' created a new user ' . $this->createdUser->name . '.'; - AuditItemService::createAuditItemForEvent( - actioningUser: $this->actioningUser, - model: $this->createdUser, - eventText: $eventText + model : $this->createdUser, + eventText: 'User ' . $this->createdUser->name . ' was created.', + teamId : $this->createdUser->current_team_id ); } } diff --git a/app/Listeners/Users/HandleUserWasCreatedEvent.php b/app/Listeners/Users/HandleUserWasCreatedEvent.php index c0dfdfa0..84c1c5bc 100644 --- a/app/Listeners/Users/HandleUserWasCreatedEvent.php +++ b/app/Listeners/Users/HandleUserWasCreatedEvent.php @@ -11,7 +11,9 @@ class HandleUserWasCreatedEvent /** * Create the event listener. */ - public function __construct() {} + public function __construct() + { + } /** * Handle the event. @@ -20,13 +22,12 @@ public function __construct() {} */ public function handle(UserWasCreated $event): void { - if (env('APP_ENV') != 'testing') { - dispatch( - new RecordUserWasCreatedAuditItem( - actioningUser: Auth::user(), - createdUser: $event->user - ) - ); - } + + dispatch( + new RecordUserWasCreatedAuditItem( + createdUser: $event->user + ) + ); + } } diff --git a/app/Models/AuditItem.php b/app/Models/AuditItem.php index a8f095d2..4c4350cd 100644 --- a/app/Models/AuditItem.php +++ b/app/Models/AuditItem.php @@ -16,7 +16,6 @@ class AuditItem extends Model 'auditable_type', 'auditable_text', 'auditable_team_id', - 'actioning_user_id', ]; public function auditable(): MorphTo diff --git a/app/Services/AuditItemService.php b/app/Services/AuditItemService.php index 3d45c045..243fa09e 100644 --- a/app/Services/AuditItemService.php +++ b/app/Services/AuditItemService.php @@ -8,24 +8,14 @@ class AuditItemService { - public static function createAuditItemForEvent(User $actioningUser, Model $model, string $eventText): AuditItem + public static function createAuditItemForEvent(Model $model, string $eventText, ?int $teamId = null): AuditItem { - $teamId = null; - - if ($model->hasAttribute('team_id')) { - $teamId = $model->team_id; - } - - if ($model->hasAttribute('current_team_id')) { - $teamId = $model->current_team_id; - } $auditItem = new AuditItem(); $auditItem->auditable_type = get_class($model); $auditItem->auditable_id = $model->id; $auditItem->auditable_text = $eventText; $auditItem->auditable_team_id = $teamId; - $auditItem->actioning_user_id = $actioningUser->id; $auditItem->save(); return $auditItem; diff --git a/database/factories/AuditItemFactory.php b/database/factories/AuditItemFactory.php index 5fb52784..66c46bad 100644 --- a/database/factories/AuditItemFactory.php +++ b/database/factories/AuditItemFactory.php @@ -23,7 +23,6 @@ public function definition(): array 'auditable_id' => $this->faker->randomDigitNotNull(), 'auditable_text' => $this->faker->randomElement(['created', 'updated', 'deleted']), 'auditable_team_id' => $this->faker->randomDigitNotNull(), - 'actioning_user_id' => $this->faker->randomDigitNotNull(), ]; } } diff --git a/database/migrations/2024_08_14_052516_create_audit_items_table.php b/database/migrations/2024_08_14_052516_create_audit_items_table.php index 371ad537..57e92b88 100644 --- a/database/migrations/2024_08_14_052516_create_audit_items_table.php +++ b/database/migrations/2024_08_14_052516_create_audit_items_table.php @@ -17,7 +17,6 @@ public function up(): void $table->string('auditable_id'); $table->string('auditable_text'); $table->unsignedBigInteger('auditable_team_id')->nullable()->index('ai_ati'); - $table->unsignedBigInteger('actioning_user_id')->index('ai_aui'); $table->timestamps(); $table->softDeletes(); diff --git a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php index 82f3f3a2..7054af37 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php @@ -79,7 +79,7 @@ public function itCanGetAllResources() $responseObject = json_decode($response->getContent(), false); - self::assertCount($num, $responseObject->data->data); + foreach ($responseObject->data->data as $auditItem) { self::assertSame($this->user->current_team_id, $auditItem->auditable_team_id); diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index f972405b..3762a31b 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -58,7 +58,7 @@ public function testAuditItemsRelation(): void $userWithAuditItems = User::with('auditItems')->find($user->id); $this->assertInstanceOf(User::class, $userWithAuditItems); - $this->assertCount($num, $userWithAuditItems->auditItems); + $userWithAuditItems->auditItems->map(function ($auditItem) use ($user) { $this->assertEquals($user->id, $auditItem->auditable_id); $this->assertSame(User::class, $auditItem->auditable_type); diff --git a/tests/Unit/Services/AuditItemServiceTest.php b/tests/Unit/Services/AuditItemServiceTest.php index 1a592235..2c88c684 100644 --- a/tests/Unit/Services/AuditItemServiceTest.php +++ b/tests/Unit/Services/AuditItemServiceTest.php @@ -42,7 +42,6 @@ public function testCreateAuditItemForEvent(): void $eventText = Str::random($num * $num); $auditItem = AuditItemService::createAuditItemForEvent( - actioningUser: $actioningUser, model: $auditableModel, eventText: $eventText, ); From af8cffe960ab531af1398e0af7a7a8256960f7e8 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:45:18 +1000 Subject: [PATCH 09/15] Removing actioningUser --- app/Jobs/RecordUserWasCreatedAuditItem.php | 1 - tests/Unit/Services/AuditItemServiceTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/Jobs/RecordUserWasCreatedAuditItem.php b/app/Jobs/RecordUserWasCreatedAuditItem.php index f93281c0..3f9a5f94 100644 --- a/app/Jobs/RecordUserWasCreatedAuditItem.php +++ b/app/Jobs/RecordUserWasCreatedAuditItem.php @@ -14,7 +14,6 @@ class RecordUserWasCreatedAuditItem implements ShouldQueue /** * Create a new job instance. * - * @param User $actioningUser * @param User $createdUser */ public function __construct(public User $createdUser) diff --git a/tests/Unit/Services/AuditItemServiceTest.php b/tests/Unit/Services/AuditItemServiceTest.php index 2c88c684..05d166b7 100644 --- a/tests/Unit/Services/AuditItemServiceTest.php +++ b/tests/Unit/Services/AuditItemServiceTest.php @@ -20,7 +20,6 @@ class AuditItemServiceTest extends TestCase #[Test] public function testCreateAuditItemForEvent(): void { - $actioningUser = User::factory()->createQuietly(); $num = rand(0, 3); From 6e0faf1d9f7a2a37f745702217b6436b342b1f64 Mon Sep 17 00:00:00 2001 From: ok200paul Date: Fri, 16 Aug 2024 07:46:09 +0000 Subject: [PATCH 10/15] Lint code --- app/Jobs/RecordUserWasCreatedAuditItem.php | 4 +--- app/Listeners/Users/HandleUserWasCreatedEvent.php | 5 +---- app/Services/AuditItemService.php | 1 - tests/Feature/API/App/AuditItems/AuditItemsGetTest.php | 2 -- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/Jobs/RecordUserWasCreatedAuditItem.php b/app/Jobs/RecordUserWasCreatedAuditItem.php index 3f9a5f94..fbc88d32 100644 --- a/app/Jobs/RecordUserWasCreatedAuditItem.php +++ b/app/Jobs/RecordUserWasCreatedAuditItem.php @@ -16,9 +16,7 @@ class RecordUserWasCreatedAuditItem implements ShouldQueue * * @param User $createdUser */ - public function __construct(public User $createdUser) - { - } + public function __construct(public User $createdUser) {} /** * Execute the job. diff --git a/app/Listeners/Users/HandleUserWasCreatedEvent.php b/app/Listeners/Users/HandleUserWasCreatedEvent.php index 84c1c5bc..fc9515f3 100644 --- a/app/Listeners/Users/HandleUserWasCreatedEvent.php +++ b/app/Listeners/Users/HandleUserWasCreatedEvent.php @@ -4,16 +4,13 @@ use App\Events\Users\UserWasCreated; use App\Jobs\RecordUserWasCreatedAuditItem; -use Auth; class HandleUserWasCreatedEvent { /** * Create the event listener. */ - public function __construct() - { - } + public function __construct() {} /** * Handle the event. diff --git a/app/Services/AuditItemService.php b/app/Services/AuditItemService.php index 243fa09e..e702da0e 100644 --- a/app/Services/AuditItemService.php +++ b/app/Services/AuditItemService.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\AuditItem; -use App\Models\User; use Illuminate\Database\Eloquent\Model; class AuditItemService diff --git a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php index 7054af37..427e25c7 100644 --- a/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php +++ b/tests/Feature/API/App/AuditItems/AuditItemsGetTest.php @@ -79,8 +79,6 @@ public function itCanGetAllResources() $responseObject = json_decode($response->getContent(), false); - - foreach ($responseObject->data->data as $auditItem) { self::assertSame($this->user->current_team_id, $auditItem->auditable_team_id); } From 9c5ffa1266cb75ba2d3d5827c95d372802b02063 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:49:50 +1000 Subject: [PATCH 11/15] Removed user audititems relation --- app/Models/User.php | 5 ----- tests/Unit/Models/UserTest.php | 34 ---------------------------------- 2 files changed, 39 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 70a540a0..b174d209 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -63,11 +63,6 @@ public function teamUsers(): HasMany return $this->hasMany(TeamUser::class); } - public function auditItems(): MorphMany - { - return $this->morphMany(AuditItem::class, 'auditable'); - } - public function currentTeam(): BelongsTo { return $this->belongsTo(Team::class); diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 3762a31b..ba2a36e4 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -31,38 +31,4 @@ public function testTeamUserRelation(): void $this->assertSame($teamUser->team_id, $userWithTeamUsers->teamUsers->first()->team_id); } - #[Test] - public function testAuditItemsRelation(): void - { - $user = User::factory()->create(); - - $num = rand(1, 10); - - AuditItem::factory($num)->create([ - 'auditable_type' => User::class, - 'auditable_id' => $user->id, - ]); - - // Same ID but different class, should NOT be returned - AuditItem::factory($num)->create([ - 'auditable_type' => Team::class, - 'auditable_id' => $user->id, - ]); - - // Same class but different ID, should NOT be returned - AuditItem::factory($num)->create([ - 'auditable_type' => User::class, - 'auditable_id' => $user->id + 1, - ]); - - $userWithAuditItems = User::with('auditItems')->find($user->id); - - $this->assertInstanceOf(User::class, $userWithAuditItems); - - $userWithAuditItems->auditItems->map(function ($auditItem) use ($user) { - $this->assertEquals($user->id, $auditItem->auditable_id); - $this->assertSame(User::class, $auditItem->auditable_type); - $this->assertInstanceOf(AuditItem::class, $auditItem); - }); - } } From 565e818bb67c96423a98a886bb25f441a36c19c7 Mon Sep 17 00:00:00 2001 From: ok200paul Date: Fri, 16 Aug 2024 07:50:53 +0000 Subject: [PATCH 12/15] Lint code --- app/Models/User.php | 1 - tests/Unit/Models/UserTest.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index b174d209..88906b8f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index ba2a36e4..53f0757c 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Models; -use App\Models\AuditItem; use App\Models\Team; use App\Models\TeamUser; use App\Models\User; @@ -30,5 +29,4 @@ public function testTeamUserRelation(): void $this->assertSame($teamUser->user_id, $userWithTeamUsers->teamUsers->first()->user_id); $this->assertSame($teamUser->team_id, $userWithTeamUsers->teamUsers->first()->team_id); } - } From 16a1e5cc33a883f8c54445ca29cf75859063a267 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:51:55 +1000 Subject: [PATCH 13/15] Update AppServiceProvider.php --- app/Providers/AppServiceProvider.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 5b352afe..3ff86096 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,9 +2,6 @@ namespace App\Providers; -use App\Events\Users\UserWasCreated; -use App\Listeners\Users\HandleUserWasCreatedEvent; -use Event; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,9 +16,6 @@ public function register(): void {} */ public function boot(): void { - Event::listen( - events: UserWasCreated::class, - listener: HandleUserWasCreatedEvent::class - ); + } } From 68fe95ab2bb8531a22c9965222aae79f611e4c97 Mon Sep 17 00:00:00 2001 From: ok200paul Date: Fri, 16 Aug 2024 07:52:52 +0000 Subject: [PATCH 14/15] Lint code --- app/Providers/AppServiceProvider.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3ff86096..8060b78d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -14,8 +14,5 @@ public function register(): void {} /** * Bootstrap any application services. */ - public function boot(): void - { - - } + public function boot(): void {} } From 3d74d176ce00001701e2a367d25a8b6b9c14e732 Mon Sep 17 00:00:00 2001 From: Paul Grimes Date: Fri, 16 Aug 2024 17:53:39 +1000 Subject: [PATCH 15/15] Added index --- .../migrations/2024_08_14_052516_create_audit_items_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2024_08_14_052516_create_audit_items_table.php b/database/migrations/2024_08_14_052516_create_audit_items_table.php index 57e92b88..ef7e1d66 100644 --- a/database/migrations/2024_08_14_052516_create_audit_items_table.php +++ b/database/migrations/2024_08_14_052516_create_audit_items_table.php @@ -14,7 +14,7 @@ public function up(): void Schema::create('audit_items', function (Blueprint $table) { $table->id(); $table->string('auditable_type')->index('ai_at'); - $table->string('auditable_id'); + $table->string('auditable_id')->index('ai_ai'); $table->string('auditable_text'); $table->unsignedBigInteger('auditable_team_id')->nullable()->index('ai_ati'); $table->timestamps();