This commit is contained in:
Ralph J. Smit
2022-07-05 18:50:34 +02:00
parent 3c510906ee
commit 5933a06dd3
17 changed files with 238 additions and 109 deletions

View File

@@ -2,56 +2,52 @@
namespace App\Actions\Site;
use App\DataTransferObjects\SiteData;
use App\Jobs\Sites\CreateSite;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Sites\CreateSite;
use Illuminate\Support\Facades\Mail;
use App\DataTransferObjects\SiteData;
use Illuminate\Database\Eloquent\Model;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use Illuminate\Support\Facades\Mail;
class CreateSiteAction
{
public function execute(SiteData $siteData): ?Site
{
$server = $this->determineServer($siteData->server_id ?? null);
$server = $this->determineServer($siteData);
if (! $server) {
if ( ! $server ) {
return null;
}
$site = $server->sites()->create($siteData->toArray());
auth()->user()->sites()->save($site);
$siteData->getUser()->sites()->save($site);
dispatch(new CreateSite($site));
auth()->user()->systemLogs()->create([
$siteData->getUser()->systemLogs()->create([
'title' => 'New site :site created',
'description' => 'A new site has been created',
])->model()->associate($site)->save();
$this->sendAdminSiteCreatedEmails($server, $site);
$this->sendAdminSiteCreatedEmails($server, $site, $siteData->getUser());
return $site;
}
protected function determineServer(?int $serverId): ?Server
protected function determineServer(SiteData $siteData): ?Server
{
if ($serverId) {
return auth()->user()->servers()->findOrFail($serverId);
if ( $siteData->server_id ) {
return $siteData->getUser()->servers()->findOrFail($siteData->server_id);
}
$server = Server::query()
->where('maximum_sites', '>', 0)
->where(function ($query) {
->where(function ($query) use ($siteData) {
return $query
->where(function ($query) {
return $query->whereHas('users', function ($query) {
return $query->where('user_id', auth()->id());
});
})
->where(fn ($query) => $query->whereHas('users', fn ($query) => $query->where('user_id', $siteData->getUser()->id)))
->orWhere(function ($query) {
return $query->doesntHave('users');
});
@@ -65,16 +61,16 @@ class CreateSiteAction
: null;
}
protected function sendAdminSiteCreatedEmails(Server $server, Model|Site $site): void
protected function sendAdminSiteCreatedEmails(Server $server, Model|Site $site, User $user): void
{
if (! setting('receive_email_on_site_creation')) {
if ( ! setting('receive_email_on_site_creation') ) {
return;
}
$admins = User::where('role', User::ADMIN)->get();
foreach ($admins as $admin) {
Mail::to($admin)->send(new AdminSiteCreatedEmail(user: auth()->user(), server: $server, site: $site));
Mail::to($admin)->send(new AdminSiteCreatedEmail(user: $user, server: $server, site: $site));
}
}
}

View File

@@ -2,53 +2,35 @@
namespace App\DataTransferObjects;
use App\Models\Server;
use App\Models\ProviderPlan;
use Illuminate\Http\Request;
use App\Models\ProviderRegion;
use App\DataTransferObjects\Support\Data;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use Illuminate\Http\Request;
use Spatie\LaravelData\Attributes\Validation\AlphaDash;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\Max;
use Symfony\Contracts\Service\Attribute\Required;
use Spatie\LaravelData\Attributes\Validation\NotIn;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\AlphaDash;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Symfony\Contracts\Service\Attribute\Required;
class ServerData extends Data
{
public function __construct(
#[Required,
StringType,
AlphaDash,
Max(40)]
#[Required, StringType, AlphaDash, Max(40)]
public string $name,
#[Required,
NotIn(0),
Exists(ProviderPlan::class, 'id')]
#[Required, NotIn(0), Exists(ProviderPlan::class, 'id')]
public int $provider_id,
#[Required,
NotIn(0),
Exists(ProviderRegion::class, 'id')]
#[Required, NotIn(0), Exists(ProviderRegion::class, 'id')]
public int $provider_region_id,
#[Required,
NotIn(0),
Exists(ProviderPlan::class, 'id')]
#[Required, NotIn(0), Exists(ProviderPlan::class, 'id')]
public int $provider_plan_id,
#[Required,
StringType,
In(['mysql', 'mariadb', 'postgresql', 'postgresql13'])]
#[Required, StringType, In(['mysql', 'mariadb', 'postgresql', 'postgresql13'])]
public string $database_type,
) {
}
) {}
public static function fromRequest(Request $request): static
{
return static::from($request->only(['provider_id', 'provider_region_id', 'provider_plan_id', 'name', 'database_type']));
}
public static function authorize(): bool
{
return auth()->user()->can('create', Server::class);
}
}

View File

@@ -2,38 +2,56 @@
namespace App\DataTransferObjects;
use App\Models\Site;
use App\Models\Server;
use App\Rules\Hostname;
use Illuminate\Http\Request;
use App\Rules\ValidateMaximumSites;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Rule;
use App\DataTransferObjects\Support\WithUser;
use App\Models\Server;
use App\Models\Site;
use App\Rules\Hostname;
use App\Rules\ValidateMaximumSites;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\Rule;
use Spatie\LaravelData\Attributes\Validation\StringType;
class SiteData extends Data
{
use WithUser;
public function __construct(
public ?int $id = null,
public ?string $status = null,
#[Exists(Server::class)]
public ?int $server_id,
#[Required,
StringType,
Rule([new Hostname(), new ValidateMaximumSites()])]
#[Exists( Server::class ), IntegerType]
public ?int $server_id = null,
#[Required, StringType, Rule( [new Hostname(), new ValidateMaximumSites()] )]
public string $domain,
) {
}
#[IntegerType]
public ?int $user_id = null,
) {}
public static function fromRequest(Request $request): static
{
return static::from($request->only(['server_id', 'domain']));
return static::from($request->only(['server_id', 'domain', 'user_id']));
}
public static function authorize(): bool
{
return dump(auth()->user()->can('create', Site::class));
if ( auth()->guest() ) {
return true;
}
return auth()->user()->can('create', Site::class);
}
public function toArray(): array
{
return Arr::only(parent::toArray(), [
'id',
'status',
'server_id',
'domain',
]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\DataTransferObjects\Support;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Auth;
trait WithUser
{
public function getUser(): ?User
{
if (! property_exists($this, 'user_id')) {
throw new Exception("Cannot retrieve user from " . $this::class);
}
return Auth::guest()
? User::find($this->user_id)
: Auth::user();
}
}

View File

@@ -2,30 +2,24 @@
namespace App\DataTransferObjects;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Max;
use App\Models\User;
use Illuminate\Support\Carbon;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Unique;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Unique;
class UserData extends Data
{
public function __construct(
public ?int $id = null,
public ?string $avatar = null,
#[Required,
StringType,
Max(255)]
#[Required, StringType, Max(255)]
public string $name,
#[Required,
StringType,
Email,
Max(255),
Unique('users')]
#[Required, StringType, Email, Max(255), Unique(User::class)]
public string $email,
public ?Carbon $created_at = null,
) {
}
) {}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Server\CreateServerAction;
use App\DataTransferObjects\ServerData;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ServerController extends Controller
{
public function create(Request $request): Response
{
$server = app(CreateServerAction::class)->execute(ServerData::validate($request));
return response(content: ['data' => ServerData::from($server)->toArray()], status: 201);
}
}

View File

@@ -20,14 +20,12 @@ class SiteController extends Controller
public function store(Request $request): Response|JsonResponse
{
$site = app(CreateSiteAction::class)->execute(
SiteData::validate($request->only('server_id', 'domain'))
);
$site = app(CreateSiteAction::class)->execute(SiteData::validate($request));
$site->refresh();
return $site
? response(content: ['data' => SiteData::from($site)], status: 201)
? response(content: ['data' => SiteData::from($site)->toArray()], status: 201)
: response()->json([
'message' => __('It seems there is no free server room for this site to take place. Please get in touch with support to resolve this.')
], 422);

View File

@@ -29,7 +29,9 @@ class ServerController extends Controller
public function store(ServerData $serverData)
{
$server = app(CreateServerAction::class)->execute($serverData);
$this->authorize('create', Server::class);
app(CreateServerAction::class)->execute($serverData);
return redirect()->route('servers.index');
}

View File

@@ -2,14 +2,14 @@
namespace App\Http\Controllers;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use App\Jobs\Sites\DeleteSite;
use App\Http\Resources\SiteResource;
use Illuminate\Support\Facades\Hash;
use App\DataTransferObjects\SiteData;
use Illuminate\Http\RedirectResponse;
use App\Actions\Site\CreateSiteAction;
use App\DataTransferObjects\SiteData;
use App\Http\Resources\SiteResource;
use App\Jobs\Sites\DeleteSite;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
class SiteController extends Controller
{
@@ -32,9 +32,9 @@ class SiteController extends Controller
]);
}
public function store(SiteData $siteData): RedirectResponse
public function store(Request $request): RedirectResponse
{
$site = app(CreateSiteAction::class)->execute($siteData);
$site = app(CreateSiteAction::class)->execute(SiteData::validate($request));
return $site
? redirect()->route('sites.index')->with('success', __('Your website is being created'))

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class SiteRequest extends FormRequest
{
public function rules(): array
{
return [
'server_id'
];
}
}

View File

@@ -2,16 +2,34 @@
namespace App\Rules;
use App\Models\User;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\DataAwareRule;
class ValidateMaximumSites implements Rule
class ValidateMaximumSites implements Rule, DataAwareRule
{
protected array $data;
public function setData($data): static
{
$this->data = $data;
return $this;
}
protected function getUser(): User
{
return $this->data['user_id'] ?? null
? User::find($this->data['user_id'])
: auth()->user();
}
public function passes($attribute, $value)
{
$package = auth()->user()->package;
$package = $this->getUser()->package;
// If the user does not have a package, it can continue
if (!$package) {
if (! $package) {
return true;
}
@@ -20,7 +38,7 @@ class ValidateMaximumSites implements Rule
return true;
}
if ($package->maximum_sites <= auth()->user()->sites()->count()) {
if ($package->maximum_sites <= $this->getUser()->sites()->count()) {
return false;
}

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\ServerController;
use App\Http\Controllers\Api\SiteController;
use App\Http\Controllers\Api\UserController;
use Illuminate\Support\Facades\Route;
@@ -11,3 +12,7 @@ Route::resource('users', UserController::class)
Route::resource('sites', SiteController::class)
->names('site')
->only('index', 'store', 'show');
Route::resource('servers', ServerController::class)
->names('server')
->only('store');

View File

@@ -0,0 +1,31 @@
<?php
use App\Models\Provider;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use App\Models\User;
use Database\Factories\PackageFactory;
use Illuminate\Support\Facades\Mail;
use function Pest\Laravel\post;
it('can create a new server', function () {
Mail::fake();
$user = User::factory()->withPackage(fn (PackageFactory $factory) => $factory->has(Provider::factory()->withRegion()->withPlan()))->create();
$provider = Provider::sole();
$region = ProviderRegion::sole();
$plan = ProviderPlan::sole();
post(route('api.server.store'), [
'ploi_id' => 2,
'provider_id' => $provider->id,
'provider_region_id' => $region->id,
'provider_plan_id' => $plan->id,
'name' => 'example-server',
'database_type' => 'postgresql',
'user_id' => $user->id,
])
->assertCreated();
});

View File

@@ -1,14 +1,14 @@
<?php
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use function Pest\Laravel\assertDatabaseHas;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use function Pest\Laravel\assertDatabaseHas;
it('can list sites', function () {
$server = Server::factory()
@@ -107,6 +107,7 @@ it('can create a site', function () {
$response = api()
->post(route('api.site.store'), [
'domain' => 'example.ploi.io',
'user_id' => $user->id,
])
->assertCreated()
->collect()
@@ -148,3 +149,23 @@ it('can create a site', function () {
],
]);
});
it('requires the user id', function () {
$user = User::factory()->withPackage()->create();
api()
->post(route('api.site.store'), [
'domain' => 'example.ploi.io',
])
->assertInvalid('user_id');
});
it('requires the domain', function () {
$user = User::factory()->withPackage()->create();
api()
->post(route('api.site.store'), [
'user_id' => 'example.ploi.io',
])
->assertInvalid('domain');
});

View File

@@ -6,11 +6,12 @@ use App\Models\Provider;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use function Pest\Laravel\post;
use function Pest\Laravel\actingAs;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Database\Factories\PackageFactory;
use Database\Factories\PackageFactory;
use function Pest\Laravel\assertDatabaseHas;
use App\Mail\Admin\Server\AdminServerCreatedEmail;
@@ -39,7 +40,9 @@ it('can create a new server', function () {
setting(['receive_email_on_server_creation' => true]);
App::forgetInstance('settings');
$user = User::factory()->withPackage(fn (PackageFactory $factory) => $factory->has(Provider::factory()->withRegion()->withPlan()))->create();
actingAs(
$user = User::factory()->withPackage(fn (PackageFactory $factory) => $factory->has(Provider::factory()->withRegion()->withPlan()))->create()
);
$provider = Provider::sole();
$region = ProviderRegion::sole();
@@ -75,7 +78,9 @@ it('can create a new server', function () {
});
it('cannot create a server without permissions', function () {
$user = User::factory()->withPackage(fn (PackageFactory $factory) => $factory->serverPermissions(['create' => false,])->has(Provider::factory()->withRegion()->withPlan()))->create();
actingAs(
$user = User::factory()->withPackage(fn (PackageFactory $factory) => $factory->serverPermissions(['create' => false,])->has(Provider::factory()->withRegion()->withPlan()))->create()
);
expect($user->can('create', Server::class))->toBeFalse();

View File

@@ -1,15 +1,16 @@
<?php
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use function Pest\Laravel\post;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
use function Pest\Laravel\post;
it('can create a new site', function () {
Mail::fake();
@@ -30,7 +31,9 @@ it('can create a new site', function () {
setting(['receive_email_on_site_creation' => true]);
App::forgetInstance('settings');
$user = User::factory()->withPackage()->create();
actingAs(
$user = User::factory()->withPackage()->create()
);
$server = Server::factory()
->ploiId('12345')
@@ -38,6 +41,7 @@ it('can create a new site', function () {
->create();
post(route('sites.store'), ['domain' => 'example.ploi.io'])
->assertSessionHasNoErrors()
->assertRedirect();
assertDatabaseHas(Site::class, [