diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php new file mode 100644 index 0000000..090a1b2 --- /dev/null +++ b/app/Models/ApiKey.php @@ -0,0 +1,36 @@ + */ + use HasFactory; + + protected function casts(): array + { + return [ + 'last_used_at' => 'datetime', + 'revoked_at' => 'datetime', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function isRevoked(): bool + { + return $this->revoked_at !== null; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 166745c..42feb92 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -84,4 +84,9 @@ public function conversionJobs(): HasMany { return $this->hasMany(ConversionJob::class); } + + public function apiKeys(): HasMany + { + return $this->hasMany(ApiKey::class); + } } diff --git a/database/factories/ApiKeyFactory.php b/database/factories/ApiKeyFactory.php new file mode 100644 index 0000000..06c7d63 --- /dev/null +++ b/database/factories/ApiKeyFactory.php @@ -0,0 +1,33 @@ + + */ +final class ApiKeyFactory extends Factory +{ + protected $model = ApiKey::class; + + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'name' => $this->faker->words(2, true), + 'token_hash' => hash('sha256', 'fc_'.$this->faker->unique()->regexify('[A-Za-z0-9]{64}')), + 'last_used_at' => null, + 'revoked_at' => null, + ]; + } + + public function revoked(): static + { + return $this->state(fn () => ['revoked_at' => now()]); + } +} diff --git a/tests/Feature/Models/ApiKeyTest.php b/tests/Feature/Models/ApiKeyTest.php new file mode 100644 index 0000000..bd33674 --- /dev/null +++ b/tests/Feature/Models/ApiKeyTest.php @@ -0,0 +1,22 @@ +create(); + $apiKey = ApiKey::factory()->for($user)->create(); + + expect($apiKey->user->is($user))->toBeTrue(); + expect($user->apiKeys()->first()->is($apiKey))->toBeTrue(); +}); + +it('checks if api key is revoked', function () { + $active = ApiKey::factory()->create(['revoked_at' => null]); + $revoked = ApiKey::factory()->create(['revoked_at' => now()]); + + expect($active->isRevoked())->toBeFalse(); + expect($revoked->isRevoked())->toBeTrue(); +});