From 96ec518200458aa5a1bcef40dd6d1cac9f677a7b Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 08:14:33 +0200 Subject: [PATCH] feat(domains): Domain model, DNS TXT verification action, pending verifier job --- app/Domains/Domain/Actions/CreateDomain.php | 21 +++++++++++ app/Domains/Domain/Actions/VerifyDomain.php | 29 +++++++++++++++ .../Domain/Jobs/VerifyPendingDomainsJob.php | 22 +++++++++++ app/Domains/Domain/Models/Domain.php | 9 +++++ database/factories/DomainFactory.php | 28 ++++++++++++++ tests/Feature/Domain/VerifyDomainTest.php | 37 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 app/Domains/Domain/Actions/CreateDomain.php create mode 100644 app/Domains/Domain/Actions/VerifyDomain.php create mode 100644 app/Domains/Domain/Jobs/VerifyPendingDomainsJob.php create mode 100644 database/factories/DomainFactory.php create mode 100644 tests/Feature/Domain/VerifyDomainTest.php diff --git a/app/Domains/Domain/Actions/CreateDomain.php b/app/Domains/Domain/Actions/CreateDomain.php new file mode 100644 index 0000000..dd72c54 --- /dev/null +++ b/app/Domains/Domain/Actions/CreateDomain.php @@ -0,0 +1,21 @@ + $workspace->id, + 'hostname' => strtolower(trim($hostname)), + 'verification_token' => Str::random(32), + 'ssl_status' => 'pending', + 'is_default' => false, + ]); + } +} diff --git a/app/Domains/Domain/Actions/VerifyDomain.php b/app/Domains/Domain/Actions/VerifyDomain.php new file mode 100644 index 0000000..0ef5310 --- /dev/null +++ b/app/Domains/Domain/Actions/VerifyDomain.php @@ -0,0 +1,29 @@ +hostname}", DNS_TXT); + + if (! is_array($records)) { + return false; + } + + foreach ($records as $record) { + if (($record['txt'] ?? '') === "nimuli-verify={$domain->verification_token}") { + $domain->update([ + 'verified_at' => now(), + 'ssl_status' => 'pending', + ]); + return true; + } + } + + return false; + } +} diff --git a/app/Domains/Domain/Jobs/VerifyPendingDomainsJob.php b/app/Domains/Domain/Jobs/VerifyPendingDomainsJob.php new file mode 100644 index 0000000..5c5c9d3 --- /dev/null +++ b/app/Domains/Domain/Jobs/VerifyPendingDomainsJob.php @@ -0,0 +1,22 @@ +where('created_at', '>=', now()->subDays(30)) + ->each(fn(Domain $domain) => $action->handle($domain)); + } +} diff --git a/app/Domains/Domain/Models/Domain.php b/app/Domains/Domain/Models/Domain.php index 594c337..22d80b6 100644 --- a/app/Domains/Domain/Models/Domain.php +++ b/app/Domains/Domain/Models/Domain.php @@ -4,9 +4,13 @@ namespace App\Domains\Domain\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Str; class Domain extends Model { + use HasFactory; + protected $guarded = ['id']; protected $casts = [ @@ -25,4 +29,9 @@ class Domain extends Model { return $this->verified_at !== null; } + + protected static function newFactory(): \Database\Factories\DomainFactory + { + return \Database\Factories\DomainFactory::new(); + } } diff --git a/database/factories/DomainFactory.php b/database/factories/DomainFactory.php new file mode 100644 index 0000000..91f0722 --- /dev/null +++ b/database/factories/DomainFactory.php @@ -0,0 +1,28 @@ +create(); + $workspace = (new \App\Domains\Workspace\Actions\CreateWorkspace)->handle($user, ['name' => 'Test']); + + return [ + 'workspace_id' => $workspace->id, + 'hostname' => $this->faker->domainName(), + 'verification_token' => Str::random(32), + 'ssl_status' => 'pending', + 'is_default' => false, + ]; + } +} diff --git a/tests/Feature/Domain/VerifyDomainTest.php b/tests/Feature/Domain/VerifyDomainTest.php new file mode 100644 index 0000000..e3a0f66 --- /dev/null +++ b/tests/Feature/Domain/VerifyDomainTest.php @@ -0,0 +1,37 @@ +create([ + 'hostname' => 'example.com', + 'verification_token' => 'abc123', + 'verified_at' => null, + ]); + + expect($domain->isVerified())->toBeFalse(); +}); + +it('reports verified domain as verified', function () { + $domain = Domain::factory()->create([ + 'hostname' => 'example.com', + 'verification_token' => 'abc123', + 'verified_at' => now(), + ]); + + expect($domain->isVerified())->toBeTrue(); +}); + +it('does not verify domain with wrong TXT record', function () { + $domain = Domain::factory()->create([ + 'hostname' => 'nonexistent-domain-that-will-fail-dns.invalid', + 'verification_token' => 'wrongtoken', + ]); + + $action = new VerifyDomain; + $result = $action->handle($domain); + + expect($result)->toBeFalse() + ->and($domain->fresh()->verified_at)->toBeNull(); +});