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); + } +}