feat(domains): Domain model, DNS TXT verification action, pending verifier job
parent
a394e0ff18
commit
96ec518200
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Domain\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Domain\Models\Domain;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateDomain
|
||||||
|
{
|
||||||
|
public function handle(Workspace $workspace, string $hostname): Domain
|
||||||
|
{
|
||||||
|
return Domain::create([
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'hostname' => strtolower(trim($hostname)),
|
||||||
|
'verification_token' => Str::random(32),
|
||||||
|
'ssl_status' => 'pending',
|
||||||
|
'is_default' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Domain\Actions;
|
||||||
|
|
||||||
|
use App\Domains\Domain\Models\Domain;
|
||||||
|
|
||||||
|
class VerifyDomain
|
||||||
|
{
|
||||||
|
public function handle(Domain $domain): bool
|
||||||
|
{
|
||||||
|
$records = @dns_get_record("_nimuli.{$domain->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Domain\Jobs;
|
||||||
|
|
||||||
|
use App\Domains\Domain\Actions\VerifyDomain;
|
||||||
|
use App\Domains\Domain\Models\Domain;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
|
||||||
|
class VerifyPendingDomainsJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable;
|
||||||
|
|
||||||
|
public function handle(VerifyDomain $action): void
|
||||||
|
{
|
||||||
|
Domain::whereNull('verified_at')
|
||||||
|
->where('created_at', '>=', now()->subDays(30))
|
||||||
|
->each(fn(Domain $domain) => $action->handle($domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,13 @@ namespace App\Domains\Domain\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Domain extends Model
|
class Domain extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $guarded = ['id'];
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|
@ -25,4 +29,9 @@ class Domain extends Model
|
||||||
{
|
{
|
||||||
return $this->verified_at !== null;
|
return $this->verified_at !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function newFactory(): \Database\Factories\DomainFactory
|
||||||
|
{
|
||||||
|
return \Database\Factories\DomainFactory::new();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Domains\Domain\Models\Domain;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class DomainFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Domain::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$user = User::factory()->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Domain\Actions\VerifyDomain;
|
||||||
|
use App\Domains\Domain\Models\Domain;
|
||||||
|
|
||||||
|
it('reports unverified domain as not verified', function () {
|
||||||
|
$domain = Domain::factory()->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();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue