API Feature (#334)

This commit is contained in:
Saeed Vaziry
2024-11-01 16:49:57 +01:00
committed by GitHub
parent da7b24640e
commit 417bf73e44
143 changed files with 36520 additions and 586 deletions

View File

@ -9,7 +9,7 @@
class CreateCronJob
{
public function create(Server $server, array $input): void
public function create(Server $server, array $input): CronJob
{
$cronJob = new CronJob([
'server_id' => $server->id,
@ -23,6 +23,8 @@ public function create(Server $server, array $input): void
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::READY;
$cronJob->save();
return $cronJob;
}
public static function rules(array $input): array

View File

@ -20,7 +20,7 @@ public function create(Server $server, array $input, array $links = []): Databas
'server_id' => $server->id,
'username' => $input['username'],
'password' => $input['password'],
'host' => isset($input['remote']) && $input['remote'] ? $input['host'] : 'localhost',
'host' => (isset($input['remote']) && $input['remote']) || isset($input['host']) ? $input['host'] : 'localhost',
'databases' => $links,
]);
/** @var Database $databaseHandler */

View File

@ -13,7 +13,7 @@ class LinkUser
/**
* @throws ValidationException
*/
public function link(DatabaseUser $databaseUser, array $input): void
public function link(DatabaseUser $databaseUser, array $input): DatabaseUser
{
if (! isset($input['databases']) || ! is_array($input['databases'])) {
$input['databases'] = [];
@ -43,6 +43,10 @@ public function link(DatabaseUser $databaseUser, array $input): void
);
$databaseUser->save();
$databaseUser->refresh();
return $databaseUser;
}
public static function rules(Server $server, array $input): array

View File

@ -6,28 +6,28 @@
use App\Enums\ServerProvider;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Project;
use App\Models\Server;
use App\Models\User;
use App\Notifications\ServerInstallationFailed;
use App\Notifications\ServerInstallationSucceed;
use App\ValidationRules\RestrictedIPAddressesRule;
use Exception;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Throwable;
class CreateServer
{
/**
* @throws Throwable
*/
public function create(User $creator, array $input): Server
public function create(User $creator, Project $project, array $input): Server
{
$server = new Server([
'project_id' => $creator->currentProject->id,
'project_id' => $project->id,
'user_id' => $creator->id,
'name' => $input['name'],
'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']],
@ -76,7 +76,9 @@ public function create(User $creator, array $input): Server
} catch (Exception $e) {
$server->provider()->delete();
DB::rollBack();
throw $e;
throw ValidationException::withMessages([
'provider' => $e->getMessage(),
]);
}
}
@ -112,7 +114,7 @@ function () use ($server) {
$bus->onConnection('ssh')->dispatch();
}
public static function rules(array $input): array
public static function rules(Project $project, array $input): array
{
$rules = [
'provider' => [
@ -132,15 +134,18 @@ public static function rules(array $input): array
],
'server_provider' => [
Rule::when(function () use ($input) {
return $input['provider'] != ServerProvider::CUSTOM;
return isset($input['provider']) && $input['provider'] != ServerProvider::CUSTOM;
}, [
'required',
'exists:server_providers,id,user_id,'.auth()->user()->id,
Rule::exists('server_providers', 'id')->where(function (Builder $query) use ($project) {
$query->where('project_id', $project->id)
->orWhereNull('project_id');
}),
]),
],
'ip' => [
Rule::when(function () use ($input) {
return $input['provider'] == ServerProvider::CUSTOM;
return isset($input['provider']) && $input['provider'] == ServerProvider::CUSTOM;
}, [
'required',
new RestrictedIPAddressesRule,
@ -148,7 +153,7 @@ public static function rules(array $input): array
],
'port' => [
Rule::when(function () use ($input) {
return $input['provider'] == ServerProvider::CUSTOM;
return isset($input['provider']) && $input['provider'] == ServerProvider::CUSTOM;
}, [
'required',
'numeric',
@ -176,6 +181,7 @@ private static function providerRules(array $input): array
{
if (
! isset($input['provider']) ||
! isset($input['server_provider']) ||
! in_array($input['provider'], config('core.server_providers')) ||
$input['provider'] == ServerProvider::CUSTOM
) {

View File

@ -2,6 +2,7 @@
namespace App\Actions\ServerProvider;
use App\Models\Project;
use App\Models\ServerProvider;
use App\Models\User;
use App\ServerProviders\ServerProvider as ServerProviderContract;
@ -14,7 +15,7 @@ class CreateServerProvider
/**
* @throws ValidationException
*/
public function create(User $user, array $input): ServerProvider
public function create(User $user, Project $project, array $input): ServerProvider
{
$provider = static::getProvider($input['provider']);
@ -33,7 +34,7 @@ public function create(User $user, array $input): ServerProvider
$serverProvider->profile = $input['name'];
$serverProvider->provider = $input['provider'];
$serverProvider->credentials = $provider->credentialData($input);
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id;
$serverProvider->save();
return $serverProvider;
@ -46,9 +47,9 @@ private static function getProvider($name): ServerProviderContract
return new $providerClass;
}
public static function rules(): array
public static function rules(array $input): array
{
return [
$rules = [
'name' => [
'required',
],
@ -58,10 +59,16 @@ public static function rules(): array
Rule::notIn('custom'),
],
];
return array_merge($rules, static::providerRules($input));
}
public static function providerRules(array $input): array
private static function providerRules(array $input): array
{
if (! isset($input['provider'])) {
return [];
}
return static::getProvider($input['provider'])->credentialValidationRules($input);
}
}

View File

@ -3,17 +3,16 @@
namespace App\Actions\ServerProvider;
use App\Models\ServerProvider;
use Exception;
use Illuminate\Validation\ValidationException;
class DeleteServerProvider
{
/**
* @throws Exception
*/
public function delete(ServerProvider $serverProvider): void
{
if ($serverProvider->servers()->exists()) {
throw new Exception('This server provider is being used by a server.');
throw ValidationException::withMessages([
'provider' => 'This server provider is being used by a server.',
]);
}
$serverProvider->delete();

View File

@ -2,17 +2,19 @@
namespace App\Actions\ServerProvider;
use App\Models\Project;
use App\Models\ServerProvider;
use App\Models\User;
class EditServerProvider
{
public function edit(ServerProvider $serverProvider, User $user, array $input): void
public function edit(ServerProvider $serverProvider, Project $project, array $input): ServerProvider
{
$serverProvider->profile = $input['name'];
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id;
$serverProvider->save();
return $serverProvider;
}
public static function rules(): array

View File

@ -8,6 +8,9 @@
class Uninstall
{
/*
* @TODO: Implement the uninstaller for all service handlers
*/
public function uninstall(Service $service): void
{
Validator::make([

View File

@ -19,9 +19,6 @@
class CreateSite
{
/**
* @throws SourceControlIsNotConnected
*/
public function create(Server $server, array $input): Site
{
DB::beginTransaction();
@ -88,13 +85,12 @@ public function create(Server $server, array $input): Site
return $site;
} catch (Exception $e) {
DB::rollBack();
throw $e;
throw ValidationException::withMessages([
'type' => $e->getMessage(),
]);
}
}
/**
* @throws ValidationException
*/
public static function rules(Server $server, array $input): array
{
$rules = [

View File

@ -2,6 +2,7 @@
namespace App\Actions\SourceControl;
use App\Models\Project;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Support\Arr;
@ -10,13 +11,13 @@
class ConnectSourceControl
{
public function connect(User $user, array $input): void
public function connect(User $user, Project $project, array $input): SourceControl
{
$sourceControl = new SourceControl([
'provider' => $input['provider'],
'profile' => $input['name'],
'url' => Arr::has($input, 'url') ? $input['url'] : null,
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
'project_id' => isset($input['global']) && $input['global'] ? null : $project->id,
]);
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
@ -29,6 +30,8 @@ public function connect(User $user, array $input): void
}
$sourceControl->save();
return $sourceControl;
}
public static function rules(array $input): array

View File

@ -3,13 +3,16 @@
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
use Illuminate\Validation\ValidationException;
class DeleteSourceControl
{
public function delete(SourceControl $sourceControl): void
{
if ($sourceControl->sites()->exists()) {
throw new \Exception('This source control is being used by a site.');
throw ValidationException::withMessages([
'source_control' => __('This source control is being used by a site.'),
]);
}
$sourceControl->delete();

View File

@ -2,59 +2,47 @@
namespace App\Actions\SourceControl;
use App\Models\Project;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class EditSourceControl
{
public function edit(SourceControl $sourceControl, User $user, array $input): void
public function edit(SourceControl $sourceControl, Project $project, array $input): SourceControl
{
$sourceControl->profile = $input['name'];
$sourceControl->url = $input['url'] ?? null;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $project->id;
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
$sourceControl->provider_data = $sourceControl->provider()->editData($input);
if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]
),
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]),
]);
}
$sourceControl->save();
return $sourceControl;
}
public static function rules(array $input): array
public static function rules(SourceControl $sourceControl, array $input): array
{
$rules = [
'name' => [
'required',
],
'provider' => [
'required',
Rule::in(config('core.source_control_providers')),
],
];
return array_merge($rules, static::providerRules($input));
return array_merge($rules, static::providerRules($sourceControl, $input));
}
/**
* @throws ValidationException
*/
private static function providerRules(array $input): array
private static function providerRules(SourceControl $sourceControl, array $input): array
{
if (! isset($input['provider'])) {
return [];
}
$sourceControl = new SourceControl([
'provider' => $input['provider'],
]);
return $sourceControl->provider()->createRules($input);
return $sourceControl->provider()->editRules($input);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Actions\StorageProvider;
use App\Models\Project;
use App\Models\StorageProvider;
use App\Models\User;
use Illuminate\Validation\Rule;
@ -12,13 +13,13 @@ class CreateStorageProvider
/**
* @throws ValidationException
*/
public function create(User $user, array $input): void
public function create(User $user, Project $project, array $input): StorageProvider
{
$storageProvider = new StorageProvider([
'user_id' => $user->id,
'provider' => $input['provider'],
'profile' => $input['name'],
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
'project_id' => isset($input['global']) && $input['global'] ? null : $project->id,
]);
$storageProvider->credentials = $storageProvider->provider()->credentialData($input);
@ -36,6 +37,8 @@ public function create(User $user, array $input): void
}
$storageProvider->save();
return $storageProvider;
}
public static function rules(array $input): array

View File

@ -3,17 +3,16 @@
namespace App\Actions\StorageProvider;
use App\Models\StorageProvider;
use Exception;
use Illuminate\Validation\ValidationException;
class DeleteStorageProvider
{
/**
* @throws Exception
*/
public function delete(StorageProvider $storageProvider): void
{
if ($storageProvider->backups()->exists()) {
throw new Exception('This storage provider is being used by a backup.');
throw ValidationException::withMessages([
'provider' => __('This storage provider is being used by a backup.'),
]);
}
$storageProvider->delete();

View File

@ -2,18 +2,20 @@
namespace App\Actions\StorageProvider;
use App\Models\Project;
use App\Models\StorageProvider;
use App\Models\User;
use Illuminate\Validation\ValidationException;
class EditStorageProvider
{
public function edit(StorageProvider $storageProvider, User $user, array $input): void
public function edit(StorageProvider $storageProvider, Project $project, array $input): StorageProvider
{
$storageProvider->profile = $input['name'];
$storageProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$storageProvider->project_id = isset($input['global']) && $input['global'] ? null : $project->id;
$storageProvider->save();
return $storageProvider;
}
/**

View File

@ -1,61 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
/**
* @deprecated
*/
class AttachTag
{
public function attach(User $user, array $input): Tag
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$tag = Tag::query()->where('name', $input['name'])->first();
if ($tag) {
if (! $taggable->tags->contains($tag->id)) {
$taggable->tags()->attach($tag->id);
}
return $tag;
}
$tag = new Tag([
'project_id' => $user->currentProject->id,
'name' => $input['name'],
'color' => config('core.tag_colors')[array_rand(config('core.tag_colors'))],
]);
$tag->save();
$taggable->tags()->attach($tag->id);
return $tag;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => [
'required',
],
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
/**
* @deprecated
*/
class DetachTag
{
public function detach(Tag $tag, array $input): void
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$taggable->tags()->detach($tag->id);
}
private function validate(array $input): void
{
Validator::make($input, [
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -21,13 +21,15 @@ public function handle(): void
return;
}
User::query()->create([
$user = User::query()->create([
'name' => $this->argument('name'),
'email' => $this->argument('email'),
'password' => bcrypt($this->argument('password')),
'role' => $this->option('role'),
]);
$user->createDefaultProject();
$this->info('User created!');
}
}

View File

@ -2,8 +2,12 @@
namespace App\Enums;
use App\Traits\Enum;
final class Database
{
use Enum;
const NONE = 'none';
const MYSQL57 = 'mysql57';

30
app/Enums/PHP.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Enums;
use App\Traits\Enum;
final class PHP
{
use Enum;
const NONE = 'none';
const V70 = '7.0';
const V71 = '7.1';
const V72 = '7.2';
const V73 = '7.3';
const V74 = '7.4';
const V80 = '8.0';
const V81 = '8.1';
const V82 = '8.2';
const V83 = '8.3';
}

View File

@ -2,7 +2,9 @@
namespace App\Exceptions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class Handler extends ExceptionHandler
@ -10,7 +12,7 @@ class Handler extends ExceptionHandler
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
* @var array<class-string<Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
@ -19,7 +21,7 @@ class Handler extends ExceptionHandler
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
//
@ -45,4 +47,13 @@ public function register(): void
//
});
}
public function render($request, Throwable $e): Response
{
if ($e instanceof ModelNotFoundException) {
abort(404, class_basename($e->getModel()).' not found.');
}
return parent::render($request, $e);
}
}

View File

@ -7,9 +7,11 @@
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Spatie\RouteAttributes\Attributes\Post;
class AgentController extends Controller
{
#[Post('api/servers/{server}/agent/{id}', name: 'api.servers.agent')]
public function __invoke(Request $request, Server $server, int $id): JsonResponse
{
$validated = $this->validate($request, [

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\CronJob\CreateCronJob;
use App\Actions\CronJob\DeleteCronJob;
use App\Http\Controllers\Controller;
use App\Http\Resources\CronJobResource;
use App\Models\CronJob;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/cron-jobs')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'cron-jobs')]
class CronJobController extends Controller
{
#[Get('/', name: 'api.projects.servers.cron-jobs', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all cron jobs.')]
#[ResponseFromApiResource(CronJobResource::class, CronJob::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [CronJob::class, $server]);
$this->validateRoute($project, $server);
return CronJobResource::collection($server->cronJobs()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.cron-jobs.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new cron job.')]
#[BodyParam(name: 'command', required: true)]
#[BodyParam(name: 'user', required: true, enum: ['root', 'vito'])]
#[BodyParam(name: 'frequency', description: 'Frequency of the cron job.', required: true, example: '* * * * *')]
#[ResponseFromApiResource(CronJobResource::class, CronJob::class)]
public function create(Request $request, Project $project, Server $server): CronJobResource
{
$this->authorize('create', [CronJob::class, $server]);
$this->validateRoute($project, $server);
$this->validate($request, CreateCronJob::rules($request->all()));
$cronJob = app(CreateCronJob::class)->create($server, $request->all());
return new CronJobResource($cronJob);
}
#[Get('{cronJob}', name: 'api.projects.servers.cron-jobs.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a cron job by ID.')]
#[ResponseFromApiResource(CronJobResource::class, CronJob::class)]
public function show(Project $project, Server $server, CronJob $cronJob): CronJobResource
{
$this->authorize('view', [$cronJob, $server]);
$this->validateRoute($project, $server, $cronJob);
return new CronJobResource($cronJob);
}
#[Delete('{cronJob}', name: 'api.projects.servers.cron-jobs.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete cron job.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, CronJob $cronJob)
{
$this->authorize('delete', [$cronJob, $server]);
$this->validateRoute($project, $server, $cronJob);
app(DeleteCronJob::class)->delete($server, $cronJob);
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?CronJob $cronJob = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($cronJob && $cronJob->server_id !== $server->id) {
abort(404, 'Firewall rule not found in server');
}
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Database\CreateDatabase;
use App\Http\Controllers\Controller;
use App\Http\Resources\DatabaseResource;
use App\Models\Database;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/databases')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'databases')]
class DatabaseController extends Controller
{
#[Get('/', name: 'api.projects.servers.databases', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all databases.')]
#[ResponseFromApiResource(DatabaseResource::class, Database::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [Database::class, $server]);
$this->validateRoute($project, $server);
return DatabaseResource::collection($server->databases()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.databases.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new database.')]
#[BodyParam(name: 'name', required: true)]
#[ResponseFromApiResource(DatabaseResource::class, Database::class)]
public function create(Request $request, Project $project, Server $server): DatabaseResource
{
$this->authorize('create', [Database::class, $server]);
$this->validateRoute($project, $server);
$this->validate($request, CreateDatabase::rules($server, $request->input()));
$database = app(CreateDatabase::class)->create($server, $request->all());
return new DatabaseResource($database);
}
#[Get('{database}', name: 'api.projects.servers.databases.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a database by ID.')]
#[ResponseFromApiResource(DatabaseResource::class, Database::class)]
public function show(Project $project, Server $server, Database $database): DatabaseResource
{
$this->authorize('view', [$database, $server]);
$this->validateRoute($project, $server, $database);
return new DatabaseResource($database);
}
#[Delete('{database}', name: 'api.projects.servers.databases.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete database.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, Database $database)
{
$this->authorize('delete', [$database, $server]);
$this->validateRoute($project, $server, $database);
$database->delete();
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?Database $database = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($database && $database->server_id !== $server->id) {
abort(404, 'Database not found in server');
}
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Database\CreateDatabaseUser;
use App\Actions\Database\LinkUser;
use App\Http\Controllers\Controller;
use App\Http\Resources\DatabaseUserResource;
use App\Models\DatabaseUser;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/database-users')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'database-users')]
class DatabaseUserController extends Controller
{
#[Get('/', name: 'api.projects.servers.database-users', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all database users.')]
#[ResponseFromApiResource(DatabaseUserResource::class, DatabaseUser::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [DatabaseUser::class, $server]);
$this->validateRoute($project, $server);
return DatabaseUserResource::collection($server->databaseUsers()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.database-users.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new database user.')]
#[BodyParam(name: 'username', required: true)]
#[BodyParam(name: 'password', required: true)]
#[BodyParam(name: 'host', description: 'Host, if it is a remote user.', example: '%')]
#[ResponseFromApiResource(DatabaseUserResource::class, DatabaseUser::class)]
public function create(Request $request, Project $project, Server $server): DatabaseUserResource
{
$this->authorize('create', [DatabaseUser::class, $server]);
$this->validateRoute($project, $server);
$this->validate($request, CreateDatabaseUser::rules($server, $request->input()));
$databaseUser = app(CreateDatabaseUser::class)->create($server, $request->all());
return new DatabaseUserResource($databaseUser);
}
#[Get('{databaseUser}', name: 'api.projects.servers.database-users.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a database user by ID.')]
#[ResponseFromApiResource(DatabaseUserResource::class, DatabaseUser::class)]
public function show(Project $project, Server $server, DatabaseUser $databaseUser): DatabaseUserResource
{
$this->authorize('view', [$databaseUser, $server]);
$this->validateRoute($project, $server, $databaseUser);
return new DatabaseUserResource($databaseUser);
}
#[Post('{databaseUser}/link', name: 'api.projects.servers.database-users.link', middleware: 'ability:write')]
#[Endpoint(title: 'link', description: 'Link to databases')]
#[BodyParam(name: 'databases', description: 'Array of database names to link to the user.', required: true)]
#[ResponseFromApiResource(DatabaseUserResource::class, DatabaseUser::class)]
public function link(Request $request, Project $project, Server $server, DatabaseUser $databaseUser): DatabaseUserResource
{
$this->authorize('update', [$databaseUser, $server]);
$this->validateRoute($project, $server, $databaseUser);
$this->validate($request, LinkUser::rules($server, $request->all()));
$databaseUser = app(LinkUser::class)->link($databaseUser, $request->all());
return new DatabaseUserResource($databaseUser);
}
#[Delete('{databaseUser}', name: 'api.projects.servers.database-users.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete database user.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, DatabaseUser $databaseUser)
{
$this->authorize('delete', [$databaseUser, $server]);
$this->validateRoute($project, $server, $databaseUser);
$databaseUser->delete();
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?DatabaseUser $databaseUser = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($databaseUser && $databaseUser->server_id !== $server->id) {
abort(404, 'Database user not found in server');
}
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\FirewallRule\CreateRule;
use App\Actions\FirewallRule\DeleteRule;
use App\Http\Controllers\Controller;
use App\Http\Resources\FirewallRuleResource;
use App\Models\FirewallRule;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/firewall-rules')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'firewall-rules')]
class FirewallRuleController extends Controller
{
#[Get('/', name: 'api.projects.servers.firewall-rules', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all firewall rules.')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [FirewallRule::class, $server]);
$this->validateRoute($project, $server);
return FirewallRuleResource::collection($server->firewallRules()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.firewall-rules.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new firewall rule.')]
#[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])]
#[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])]
#[BodyParam(name: 'port', required: true)]
#[BodyParam(name: 'source', required: true)]
#[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function create(Request $request, Project $project, Server $server): FirewallRuleResource
{
$this->authorize('create', [FirewallRule::class, $server]);
$this->validateRoute($project, $server);
$this->validate($request, CreateRule::rules());
$firewallRule = app(CreateRule::class)->create($server, $request->all());
return new FirewallRuleResource($firewallRule);
}
#[Get('{firewallRule}', name: 'api.projects.servers.firewall-rules.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a firewall rule by ID.')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function show(Project $project, Server $server, FirewallRule $firewallRule): FirewallRuleResource
{
$this->authorize('view', [$firewallRule, $server]);
$this->validateRoute($project, $server, $firewallRule);
return new FirewallRuleResource($firewallRule);
}
#[Delete('{firewallRule}', name: 'api.projects.servers.firewall-rules.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete firewall rule.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, FirewallRule $firewallRule)
{
$this->authorize('delete', [$firewallRule, $server]);
$this->validateRoute($project, $server, $firewallRule);
app(DeleteRule::class)->delete($server, $firewallRule);
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?FirewallRule $firewallRule = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($firewallRule && $firewallRule->server_id !== $server->id) {
abort(404, 'Firewall rule not found in server');
}
}
}

View File

@ -11,10 +11,12 @@
use App\Notifications\SourceControlDisconnected;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Spatie\RouteAttributes\Attributes\Any;
use Throwable;
class GitHookController extends Controller
{
#[Any('api/git-hooks', name: 'api.git-hooks')]
public function __invoke(Request $request)
{
if (! $request->input('secret')) {

View File

@ -3,9 +3,17 @@
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Unauthenticated;
use Spatie\RouteAttributes\Attributes\Get;
#[Group(name: 'general')]
class HealthController extends Controller
{
#[Get('api/health', name: 'api.health')]
#[Unauthenticated]
#[Endpoint(title: 'health-check')]
public function __invoke()
{
return response()->json([

View File

@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Projects\CreateProject;
use App\Actions\Projects\DeleteProject;
use App\Actions\Projects\UpdateProject;
use App\Http\Controllers\Controller;
use App\Http\Resources\ProjectResource;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Response;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Put;
#[Middleware('auth:sanctum')]
#[Group(name: 'projects')]
class ProjectController extends Controller
{
#[Get('api/projects', name: 'api.projects.index', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all projects.')]
#[ResponseFromApiResource(ProjectResource::class, Project::class, collection: true, paginate: 25)]
public function index(): ResourceCollection
{
$this->authorize('viewAny', Project::class);
return ProjectResource::collection(Project::all());
}
#[Post('api/projects', name: 'api.projects.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new project.')]
#[BodyParam(name: 'name', description: 'The name of the project.', required: true)]
#[ResponseFromApiResource(ProjectResource::class, Project::class)]
public function create(Request $request): ProjectResource
{
$this->authorize('create', Project::class);
$this->validate($request, CreateProject::rules());
$project = app(CreateProject::class)->create(auth()->user(), $request->all());
return new ProjectResource($project);
}
#[Get('api/projects/{project}', name: 'api.projects.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a project by ID.')]
#[ResponseFromApiResource(ProjectResource::class, Project::class)]
public function show(Project $project): ProjectResource
{
$this->authorize('view', $project);
return new ProjectResource($project);
}
#[Put('api/projects/{project}', name: 'api.projects.update', middleware: 'ability:write')]
#[Endpoint(title: 'update', description: 'Update project.')]
#[BodyParam(name: 'name', description: 'The name of the project.', required: true)]
#[ResponseFromApiResource(ProjectResource::class, Project::class)]
public function update(Request $request, Project $project): ProjectResource
{
$this->authorize('update', $project);
$this->validate($request, UpdateProject::rules($project));
$project = app(UpdateProject::class)->update($project, $request->all());
return new ProjectResource($project);
}
#[Delete('api/projects/{project}', name: 'api.projects.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete project.')]
#[\Knuckles\Scribe\Attributes\Response(status: 204)]
public function delete(Project $project): Response
{
$this->authorize('delete', $project);
app(DeleteProject::class)->delete(auth()->user(), $project);
return response()->noContent();
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Server\CreateServer;
use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Enums\Database;
use App\Enums\PHP;
use App\Enums\ServerProvider;
use App\Enums\ServerType;
use App\Enums\Webserver;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServerResource;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'servers')]
class ServerController extends Controller
{
#[Get('/', name: 'api.projects.servers', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all servers in a project.')]
#[ResponseFromApiResource(ServerResource::class, Server::class, collection: true, paginate: 25)]
public function index(Project $project): ResourceCollection
{
$this->authorize('viewAny', [Server::class, $project]);
return ServerResource::collection($project->servers()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new server.')]
#[BodyParam(name: 'provider', description: 'The server provider type', required: true)]
#[BodyParam(name: 'server_provider', description: 'If the provider is not custom, the ID of the server provider profile', enum: [ServerProvider::CUSTOM, ServerProvider::HETZNER, ServerProvider::DIGITALOCEAN, ServerProvider::LINODE, ServerProvider::VULTR])]
#[BodyParam(name: 'region', description: 'Provider region if the provider is not custom')]
#[BodyParam(name: 'plan', description: 'Provider plan if the provider is not custom')]
#[BodyParam(name: 'ip', description: 'SSH IP address if the provider is custom')]
#[BodyParam(name: 'port', description: 'SSH Port if the provider is custom')]
#[BodyParam(name: 'name', description: 'The name of the server.', required: true)]
#[BodyParam(name: 'os', description: 'The os of the server', required: true)]
#[BodyParam(name: 'type', description: 'Server type', required: true, enum: [ServerType::REGULAR, ServerType::DATABASE])]
#[BodyParam(name: 'webserver', description: 'Web server', required: true, enum: [Webserver::NONE, Webserver::NGINX])]
#[BodyParam(name: 'database', description: 'Database', required: true, enum: [Database::NONE, Database::MYSQL57, Database::MYSQL80, Database::MARIADB103, Database::MARIADB104, Database::MARIADB103, Database::POSTGRESQL12, Database::POSTGRESQL13, Database::POSTGRESQL14, Database::POSTGRESQL15, Database::POSTGRESQL16], )]
#[BodyParam(name: 'php', description: 'PHP version', required: true, enum: [PHP::V70, PHP::V71, PHP::V72, PHP::V73, PHP::V74, PHP::V80, PHP::V81, PHP::V82, PHP::V83])]
#[ResponseFromApiResource(ServerResource::class, Server::class)]
public function create(Request $request, Project $project): ServerResource
{
$this->authorize('create', [Server::class, $project]);
$this->validate($request, CreateServer::rules($project, $request->input()));
$server = app(CreateServer::class)->create(auth()->user(), $project, $request->all());
return new ServerResource($server);
}
#[Get('{server}', name: 'api.projects.servers.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a server by ID.')]
#[ResponseFromApiResource(ServerResource::class, Server::class)]
public function show(Project $project, Server $server): ServerResource
{
$this->authorize('view', [$server, $project]);
$this->validateRoute($project, $server);
return new ServerResource($server);
}
#[Post('{server}/reboot', name: 'api.projects.servers.reboot', middleware: 'ability:write')]
#[Endpoint(title: 'reboot', description: 'Reboot a server.')]
#[Response(status: 204)]
public function reboot(Project $project, Server $server)
{
$this->authorize('update', [$server, $project]);
$this->validateRoute($project, $server);
app(RebootServer::class)->reboot($server);
return response()->noContent();
}
#[Post('{server}/upgrade', name: 'api.projects.servers.upgrade', middleware: 'ability:write')]
#[Endpoint(title: 'upgrade', description: 'Upgrade server.')]
#[Response(status: 204)]
public function upgrade(Project $project, Server $server)
{
$this->authorize('update', [$server, $project]);
$this->validateRoute($project, $server);
app(Update::class)->update($server);
return response()->noContent();
}
#[Delete('{server}', name: 'api.projects.servers.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete server.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server)
{
$this->authorize('delete', [$server, $project]);
$this->validateRoute($project, $server);
$server->delete();
return response()->noContent();
}
private function validateRoute(Project $project, Server $server): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\ServerProvider\CreateServerProvider;
use App\Actions\ServerProvider\DeleteServerProvider;
use App\Actions\ServerProvider\EditServerProvider;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServerProviderResource;
use App\Models\Project;
use App\Models\ServerProvider;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('api/projects/{project}/server-providers')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'server-providers')]
class ServerProviderController extends Controller
{
#[Get('/', name: 'api.projects.server-providers', middleware: 'ability:read')]
#[Endpoint(title: 'list')]
#[ResponseFromApiResource(ServerProviderResource::class, ServerProvider::class, collection: true, paginate: 25)]
public function index(Project $project): ResourceCollection
{
$this->authorize('viewAny', ServerProvider::class);
$serverProviders = ServerProvider::getByProjectId($project->id)->simplePaginate(25);
return ServerProviderResource::collection($serverProviders);
}
#[Post('/', name: 'api.projects.server-providers.create', middleware: 'ability:write')]
#[Endpoint(title: 'create')]
#[BodyParam(name: 'provider', description: 'The provider (aws, linode, hetzner, digitalocean, vultr, ...)', required: true)]
#[BodyParam(name: 'name', description: 'The name of the server provider.', required: true)]
#[BodyParam(name: 'token', description: 'The token if provider requires api token')]
#[BodyParam(name: 'key', description: 'The key if provider requires key')]
#[BodyParam(name: 'secret', description: 'The secret if provider requires key')]
#[ResponseFromApiResource(ServerProviderResource::class, ServerProvider::class)]
public function create(Request $request, Project $project): ServerProviderResource
{
$this->authorize('create', ServerProvider::class);
$this->validate($request, CreateServerProvider::rules($request->all()));
$serverProvider = app(CreateServerProvider::class)->create(auth()->user(), $project, $request->all());
return new ServerProviderResource($serverProvider);
}
#[Get('{serverProvider}', name: 'api.projects.server-providers.show', middleware: 'ability:read')]
#[Endpoint(title: 'show')]
#[ResponseFromApiResource(ServerProviderResource::class, ServerProvider::class)]
public function show(Project $project, ServerProvider $serverProvider)
{
$this->authorize('view', $serverProvider);
$this->validateRoute($project, $serverProvider);
return new ServerProviderResource($serverProvider);
}
#[Put('{serverProvider}', name: 'api.projects.server-providers.update', middleware: 'ability:write')]
#[Endpoint(title: 'update')]
#[BodyParam(name: 'name', description: 'The name of the server provider.', required: true)]
#[BodyParam(name: 'global', description: 'Accessible in all projects', enum: [true, false])]
#[ResponseFromApiResource(ServerProviderResource::class, ServerProvider::class)]
public function update(Request $request, Project $project, ServerProvider $serverProvider)
{
$this->authorize('update', $serverProvider);
$this->validateRoute($project, $serverProvider);
$this->validate($request, EditServerProvider::rules());
$serverProvider = app(EditServerProvider::class)->edit($serverProvider, $project, $request->all());
return new ServerProviderResource($serverProvider);
}
#[Delete('{serverProvider}', name: 'api.projects.server-providers.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete')]
#[Response(status: 204)]
public function delete(Project $project, ServerProvider $serverProvider)
{
$this->authorize('delete', $serverProvider);
$this->validateRoute($project, $serverProvider);
app(DeleteServerProvider::class)->delete($serverProvider);
return response()->noContent();
}
private function validateRoute(Project $project, ServerProvider $serverProvider): void
{
if (! $serverProvider->project_id) {
return;
}
if ($project->id !== $serverProvider->project_id) {
abort(404, 'Server provider not found in project');
}
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\SshKey\CreateSshKey;
use App\Actions\SshKey\DeleteKeyFromServer;
use App\Actions\SshKey\DeployKeyToServer;
use App\Http\Controllers\Controller;
use App\Http\Resources\SshKeyResource;
use App\Models\Project;
use App\Models\Server;
use App\Models\SshKey;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/ssh-keys')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'ssh-keys')]
class ServerSSHKeyController extends Controller
{
#[Get('/', name: 'api.projects.servers.ssh-keys', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all ssh keys.')]
#[ResponseFromApiResource(SshKeyResource::class, SshKey::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAnyServer', [SshKey::class, $server]);
$this->validateRoute($project, $server);
return SshKeyResource::collection($server->sshKeys()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.ssh-keys.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Deploy ssh key to server.')]
#[BodyParam(name: 'key_id', description: 'The ID of the key.')]
#[BodyParam(name: 'name', description: 'Key name, required if key_id is not provided.')]
#[BodyParam(name: 'public_key', description: 'Public Key, required if key_id is not provided.')]
#[ResponseFromApiResource(SshKeyResource::class, SshKey::class)]
public function create(Request $request, Project $project, Server $server): SshKeyResource
{
$this->authorize('create', [SshKey::class, $server]);
$this->validateRoute($project, $server);
$sshKey = null;
if ($request->has('key_id')) {
$this->validate($request, DeployKeyToServer::rules($request->user(), $server));
$sshKey = $request->user()->sshKeys()->findOrFail($request->key_id);
}
if (! $sshKey) {
$this->validate($request, CreateSshKey::rules());
$sshKey = app(CreateSshKey::class)->create($request->user(), $request->all());
}
app(DeployKeyToServer::class)->deploy($server, ['key_id' => $sshKey->id]);
return new SshKeyResource($sshKey);
}
#[Delete('{sshKey}', name: 'api.projects.servers.ssh-keys.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete ssh key from server.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, SshKey $sshKey)
{
$this->authorize('delete', [$sshKey, $server]);
$this->validateRoute($project, $server);
app(DeleteKeyFromServer::class)->delete($server, $sshKey);
return response()->noContent();
}
private function validateRoute(Project $project, Server $server): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Service\Manage;
use App\Actions\Service\Uninstall;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServiceResource;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/services')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'services')]
class ServiceController extends Controller
{
#[Get('/', name: 'api.projects.servers.services', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all services.')]
#[ResponseFromApiResource(ServiceResource::class, Service::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [Service::class, $server]);
$this->validateRoute($project, $server);
return ServiceResource::collection($server->services()->simplePaginate(25));
}
#[Get('{service}', name: 'api.projects.servers.services.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a service by ID.')]
#[ResponseFromApiResource(ServiceResource::class, Service::class)]
public function show(Project $project, Server $server, Service $service): ServiceResource
{
$this->authorize('view', [$service, $server]);
$this->validateRoute($project, $server, $service);
return new ServiceResource($service);
}
#[Post('{service}/start', name: 'api.projects.servers.services.start', middleware: 'ability:write')]
#[Endpoint(title: 'start', description: 'Start service.')]
#[Response(status: 204)]
public function start(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('update', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Manage::class)->start($service);
return response()->noContent();
}
#[Post('{service}/stop', name: 'api.projects.servers.services.stop', middleware: 'ability:write')]
#[Endpoint(title: 'stop', description: 'Stop service.')]
#[Response(status: 204)]
public function stop(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('update', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Manage::class)->stop($service);
return response()->noContent();
}
#[Post('{service}/restart', name: 'api.projects.servers.services.restart', middleware: 'ability:write')]
#[Endpoint(title: 'restart', description: 'Restart service.')]
#[Response(status: 204)]
public function restart(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('update', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Manage::class)->restart($service);
return response()->noContent();
}
#[Post('{service}/enable', name: 'api.projects.servers.services.enable', middleware: 'ability:write')]
#[Endpoint(title: 'enable', description: 'Enable service.')]
#[Response(status: 204)]
public function enable(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('update', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Manage::class)->enable($service);
return response()->noContent();
}
#[Post('{service}/disable', name: 'api.projects.servers.services.disable', middleware: 'ability:write')]
#[Endpoint(title: 'disable', description: 'Disable service.')]
#[Response(status: 204)]
public function disable(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('update', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Manage::class)->disable($service);
return response()->noContent();
}
#[Delete('{service}', name: 'api.projects.servers.services.uninstall', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete service.')]
#[Response(status: 204)]
public function uninstall(Project $project, Server $server, Service $service): \Illuminate\Http\Response
{
$this->authorize('delete', [$service, $server]);
$this->validateRoute($project, $server, $service);
app(Uninstall::class)->uninstall($service);
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?Service $service = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($service && $service->server_id !== $server->id) {
abort(404, 'Service not found in server');
}
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\Site\CreateSite;
use App\Enums\SiteType;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServerResource;
use App\Http\Resources\SiteResource;
use App\Models\Project;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('api/projects/{project}/servers/{server}/sites')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'sites')]
class SiteController extends Controller
{
#[Get('/', name: 'api.projects.servers.sites', middleware: 'ability:read')]
#[Endpoint(title: 'list', description: 'Get all sites.')]
#[ResponseFromApiResource(SiteResource::class, Site::class, collection: true, paginate: 25)]
public function index(Project $project, Server $server): ResourceCollection
{
$this->authorize('viewAny', [Site::class, $server]);
$this->validateRoute($project, $server);
return SiteResource::collection($server->sites()->simplePaginate(25));
}
#[Post('/', name: 'api.projects.servers.sites.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new site.')]
#[BodyParam(name: 'type', required: true, enum: [SiteType::PHP, SiteType::PHP_BLANK, SiteType::PHPMYADMIN, SiteType::LARAVEL, SiteType::WORDPRESS])]
#[BodyParam(name: 'domain', required: true)]
#[BodyParam(name: 'aliases', type: 'array')]
#[BodyParam(name: 'php_version', description: 'One of the installed PHP Versions', required: true, example: '7.4')]
#[BodyParam(name: 'web_directory', description: 'Required for PHP and Laravel sites', example: 'public')]
#[BodyParam(name: 'source_control', description: 'Source control ID, Required for Sites which support source control')]
#[BodyParam(name: 'repository', description: 'Repository, Required for Sites which support source control', example: 'organization/repository')]
#[BodyParam(name: 'branch', description: 'Branch, Required for Sites which support source control', example: 'main')]
#[BodyParam(name: 'composer', type: 'boolean', description: 'Run composer if site supports composer', example: true)]
#[BodyParam(name: 'version', description: 'Version, if the site type requires a version like PHPMyAdmin', example: '5.2.1')]
#[ResponseFromApiResource(SiteResource::class, Site::class)]
public function create(Request $request, Project $project, Server $server): SiteResource
{
$this->authorize('create', [Site::class, $server]);
$this->validateRoute($project, $server);
$this->validate($request, CreateSite::rules($server, $request->input()));
$site = app(CreateSite::class)->create($server, $request->all());
return new SiteResource($site);
}
#[Get('{site}', name: 'api.projects.servers.sites.show', middleware: 'ability:read')]
#[Endpoint(title: 'show', description: 'Get a site by ID.')]
#[ResponseFromApiResource(SiteResource::class, Site::class)]
public function show(Project $project, Server $server, Site $site): ServerResource
{
$this->authorize('view', [$site, $server]);
$this->validateRoute($project, $server, $site);
return new ServerResource($server);
}
#[Delete('{site}', name: 'api.projects.servers.sites.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete', description: 'Delete site.')]
#[Response(status: 204)]
public function delete(Project $project, Server $server, Site $site)
{
$this->authorize('delete', [$site, $server]);
$this->validateRoute($project, $server, $site);
$site->delete();
return response()->noContent();
}
private function validateRoute(Project $project, Server $server, ?Site $site = null): void
{
if ($project->id !== $server->project_id) {
abort(404, 'Server not found in project');
}
if ($site && $site->server_id !== $server->id) {
abort(404, 'Site not found in server');
}
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\SourceControl\ConnectSourceControl;
use App\Actions\SourceControl\DeleteSourceControl;
use App\Actions\SourceControl\EditSourceControl;
use App\Http\Controllers\Controller;
use App\Http\Resources\SourceControlResource;
use App\Models\Project;
use App\Models\SourceControl;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('api/projects/{project}/source-controls')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'source-controls')]
class SourceControlController extends Controller
{
#[Get('/', name: 'api.projects.source-controls', middleware: 'ability:read')]
#[Endpoint(title: 'list')]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class, collection: true, paginate: 25)]
public function index(Project $project): ResourceCollection
{
$this->authorize('viewAny', SourceControl::class);
$sourceControls = SourceControl::getByProjectId($project->id)->simplePaginate(25);
return SourceControlResource::collection($sourceControls);
}
#[Post('/', name: 'api.projects.source-controls.create', middleware: 'ability:write')]
#[Endpoint(title: 'create')]
#[BodyParam(name: 'provider', description: 'The provider', required: true, enum: [\App\Enums\SourceControl::GITLAB, \App\Enums\SourceControl::GITHUB, \App\Enums\SourceControl::BITBUCKET])]
#[BodyParam(name: 'name', description: 'The name of the storage provider.', required: true)]
#[BodyParam(name: 'token', description: 'The token if provider requires api token')]
#[BodyParam(name: 'url', description: 'The URL if the provider is Gitlab and it is self-hosted')]
#[BodyParam(name: 'username', description: 'The username if the provider is Bitbucket')]
#[BodyParam(name: 'password', description: 'The password if the provider is Bitbucket')]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class)]
public function create(Request $request, Project $project): SourceControlResource
{
$this->authorize('create', SourceControl::class);
$this->validate($request, ConnectSourceControl::rules($request->all()));
$sourceControl = app(ConnectSourceControl::class)->connect(auth()->user(), $project, $request->all());
return new SourceControlResource($sourceControl);
}
#[Get('{sourceControl}', name: 'api.projects.source-controls.show', middleware: 'ability:read')]
#[Endpoint(title: 'show')]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class)]
public function show(Project $project, SourceControl $sourceControl)
{
$this->authorize('view', $sourceControl);
$this->validateRoute($project, $sourceControl);
return new SourceControlResource($sourceControl);
}
#[Put('{sourceControl}', name: 'api.projects.source-controls.update', middleware: 'ability:write')]
#[Endpoint(title: 'update')]
#[BodyParam(name: 'name', description: 'The name of the storage provider.', required: true)]
#[BodyParam(name: 'token', description: 'The token if provider requires api token')]
#[BodyParam(name: 'url', description: 'The URL if the provider is Gitlab and it is self-hosted')]
#[BodyParam(name: 'username', description: 'The username if the provider is Bitbucket')]
#[BodyParam(name: 'password', description: 'The password if the provider is Bitbucket')]
#[BodyParam(name: 'global', description: 'Accessible in all projects', enum: [true, false])]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class)]
public function update(Request $request, Project $project, SourceControl $sourceControl)
{
$this->authorize('update', $sourceControl);
$this->validateRoute($project, $sourceControl);
$this->validate($request, EditSourceControl::rules($sourceControl, $request->all()));
$sourceControl = app(EditSourceControl::class)->edit($sourceControl, $project, $request->all());
return new SourceControlResource($sourceControl);
}
#[Delete('{sourceControl}', name: 'api.projects.source-controls.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete')]
#[Response(status: 204)]
public function delete(Project $project, SourceControl $sourceControl)
{
$this->authorize('delete', $sourceControl);
$this->validateRoute($project, $sourceControl);
app(DeleteSourceControl::class)->delete($sourceControl);
return response()->noContent();
}
private function validateRoute(Project $project, SourceControl $sourceControl): void
{
if (! $sourceControl->project_id) {
return;
}
if ($project->id !== $sourceControl->project_id) {
abort(404, 'Source Control not found in project');
}
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\API;
use App\Actions\StorageProvider\CreateStorageProvider;
use App\Actions\StorageProvider\DeleteStorageProvider;
use App\Actions\StorageProvider\EditStorageProvider;
use App\Http\Controllers\Controller;
use App\Http\Resources\StorageProviderResource;
use App\Models\Project;
use App\Models\StorageProvider;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Response;
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('api/projects/{project}/storage-providers')]
#[Middleware(['auth:sanctum', 'can-see-project'])]
#[Group(name: 'storage-providers')]
class StorageProviderController extends Controller
{
#[Get('/', name: 'api.projects.storage-providers', middleware: 'ability:read')]
#[Endpoint(title: 'list')]
#[ResponseFromApiResource(StorageProviderResource::class, StorageProvider::class, collection: true, paginate: 25)]
public function index(Project $project): ResourceCollection
{
$this->authorize('viewAny', StorageProvider::class);
$storageProviders = StorageProvider::getByProjectId($project->id)->simplePaginate(25);
return StorageProviderResource::collection($storageProviders);
}
#[Post('/', name: 'api.projects.storage-providers.create', middleware: 'ability:write')]
#[Endpoint(title: 'create')]
#[BodyParam(name: 'provider', description: 'The provider (aws, linode, hetzner, digitalocean, vultr, ...)', required: true)]
#[BodyParam(name: 'name', description: 'The name of the storage provider.', required: true)]
#[BodyParam(name: 'token', description: 'The token if provider requires api token')]
#[BodyParam(name: 'key', description: 'The key if provider requires key')]
#[BodyParam(name: 'secret', description: 'The secret if provider requires key')]
#[ResponseFromApiResource(StorageProviderResource::class, StorageProvider::class)]
public function create(Request $request, Project $project): StorageProviderResource
{
$this->authorize('create', StorageProvider::class);
$this->validate($request, CreateStorageProvider::rules($request->all()));
$storageProvider = app(CreateStorageProvider::class)->create(auth()->user(), $project, $request->all());
return new StorageProviderResource($storageProvider);
}
#[Get('{storageProvider}', name: 'api.projects.storage-providers.show', middleware: 'ability:read')]
#[Endpoint(title: 'show')]
#[ResponseFromApiResource(StorageProviderResource::class, StorageProvider::class)]
public function show(Project $project, StorageProvider $storageProvider)
{
$this->authorize('view', $storageProvider);
$this->validateRoute($project, $storageProvider);
return new StorageProviderResource($storageProvider);
}
#[Put('{storageProvider}', name: 'api.projects.storage-providers.update', middleware: 'ability:write')]
#[Endpoint(title: 'update')]
#[BodyParam(name: 'name', description: 'The name of the storage provider.', required: true)]
#[BodyParam(name: 'global', description: 'Accessible in all projects', enum: [true, false])]
#[ResponseFromApiResource(StorageProviderResource::class, StorageProvider::class)]
public function update(Request $request, Project $project, StorageProvider $storageProvider)
{
$this->authorize('update', $storageProvider);
$this->validateRoute($project, $storageProvider);
$this->validate($request, EditStorageProvider::rules());
$storageProvider = app(EditStorageProvider::class)->edit($storageProvider, $project, $request->all());
return new StorageProviderResource($storageProvider);
}
#[Delete('{storageProvider}', name: 'api.projects.storage-providers.delete', middleware: 'ability:write')]
#[Endpoint(title: 'delete')]
#[Response(status: 204)]
public function delete(Project $project, StorageProvider $storageProvider)
{
$this->authorize('delete', $storageProvider);
$this->validateRoute($project, $storageProvider);
app(DeleteStorageProvider::class)->delete($storageProvider);
return response()->noContent();
}
private function validateRoute(Project $project, StorageProvider $storageProvider): void
{
if (! $storageProvider->project_id) {
return;
}
if ($project->id !== $storageProvider->project_id) {
abort(404, 'Storage provider not found in project');
}
}
}

View File

@ -5,9 +5,13 @@
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
#[Middleware('auth')]
class ConsoleController extends Controller
{
#[Post('/{server}/console', name: 'servers.console.run')]
public function run(Server $server, Request $request)
{
$this->authorize('update', $server);

View File

@ -62,5 +62,9 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
'has-project' => \App\Http\Middleware\HasProjectMiddleware::class,
'can-see-project' => \App\Http\Middleware\CanSeeProjectMiddleware::class,
];
}

View File

@ -12,6 +12,6 @@ class Authenticate extends Middleware
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
return $request->expectsJson() ? null : url('/');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Models\Project;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
class CanSeeProjectMiddleware
{
public function handle(Request $request, Closure $next)
{
/** @var User $user */
$user = $request->user();
/** @var Project $project */
$project = $request->route('project');
if (! $user->can('view', $project)) {
abort(403, 'You do not have permission to view this project.');
}
return $next($request);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
class HasProjectMiddleware
{
public function handle(Request $request, Closure $next)
{
/** @var ?User $user */
$user = $request->user();
if (! $user) {
return $next($request);
}
if (! $user->currentProject) {
if ($user->allProjects()->count() > 0) {
$user->current_project_id = $user->projects->first()->id;
$user->save();
return $next($request);
}
abort(403, 'You must have a project to access the panel.');
}
return $next($request);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use App\Models\CronJob;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin CronJob */
class CronJobResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'command' => $this->command,
'user' => $this->user,
'frequency' => $this->frequency,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use App\Models\Database;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Database */
class DatabaseResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'name' => $this->name,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use App\Models\DatabaseUser;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin DatabaseUser */
class DatabaseUserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'username' => $this->username,
'databases' => $this->databases,
'host' => $this->host,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use App\Models\FirewallRule;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin FirewallRule */
class FirewallRuleResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'type' => $this->type,
'protocol' => $this->protocol,
'port' => $this->port,
'source' => $this->source,
'mask' => $this->mask,
'note' => $this->note,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Project */
class ProjectResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use App\Models\ServerProvider;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin ServerProvider */
class ServerProviderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'project_id' => $this->project_id,
'global' => is_null($this->project_id),
'name' => $this->profile,
'provider' => $this->provider,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Resources;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Server */
class ServerResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'project_id' => $this->project_id,
'user_id' => $this->user_id,
'provider_id' => $this->provider_id,
'name' => $this->name,
'ssh_user' => $this->ssh_user,
'ip' => $this->ip,
'local_ip' => $this->local_ip,
'port' => $this->port,
'os' => $this->os,
'type' => $this->type,
'type_data' => $this->type_data,
'provider' => $this->provider,
'provider_data' => $this->provider_data,
'public_key' => $this->public_key,
'status' => $this->status,
'auto_update' => $this->auto_update,
'available_updates' => $this->available_updates,
'security_updates' => $this->security_updates,
'progress' => $this->progress,
'progress_step' => $this->progress_step,
'updates' => $this->updates,
'last_update_check' => $this->last_update_check,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Service */
class ServiceResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'type' => $this->type,
'type_data' => $this->type_data,
'name' => $this->name,
'version' => $this->version,
'unit' => $this->unit,
'status' => $this->status,
'is_default' => $this->is_default,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Site */
class SiteResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->server_id,
'source_control_id' => $this->source_control_id,
'type' => $this->type,
'type_data' => $this->type_data,
'domain' => $this->domain,
'aliases' => $this->aliases,
'web_directory' => $this->web_directory,
'path' => $this->path,
'php_version' => $this->php_version,
'repository' => $this->repository,
'branch' => $this->branch,
'status' => $this->status,
'port' => $this->port,
'progress' => $this->progress,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use App\Models\SourceControl;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin SourceControl */
class SourceControlResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'project_id' => $this->project_id,
'global' => is_null($this->project_id),
'name' => $this->profile,
'provider' => $this->provider,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use App\Models\SshKey;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin SshKey */
class SshKeyResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'user' => $this->user_id ? new UserResource($this->user) : null,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use App\Models\StorageProvider;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin StorageProvider */
class StorageProviderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'project_id' => $this->project_id,
'global' => is_null($this->project_id),
'name' => $this->profile,
'provider' => $this->provider,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin User */
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use App\Traits\HasTimezoneTimestamps;
use Carbon\Carbon;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
/**
* @property int $id
* @property string $tokenable_type
* @property int $tokenable_id
* @property string $name
* @property string $token
* @property array $abilities
* @property Carbon $last_used_at
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class PersonalAccessToken extends SanctumPersonalAccessToken
{
use HasTimezoneTimestamps;
}

View File

@ -16,6 +16,7 @@
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
/**
* @property int $id
@ -43,6 +44,7 @@
*/
class User extends Authenticatable implements FilamentUser
{
use HasApiTokens;
use HasFactory;
use HasTimezoneTimestamps;
use Notifiable;
@ -67,22 +69,6 @@ class User extends Authenticatable implements FilamentUser
protected $appends = [
];
public static function boot(): void
{
parent::boot();
static::created(function (User $user) {
if ($user->projects()->count() === 0) {
$user->createDefaultProject();
$user->refresh();
}
if (! $user->currentProject) {
$user->current_project_id = $user->projects()->first()->id;
$user->save();
}
});
}
public function servers(): HasMany
{
return $this->hasMany(Server::class);
@ -118,9 +104,19 @@ public function storageProvider(string $provider): HasOne
return $this->hasOne(StorageProvider::class)->where('provider', $provider);
}
public function allProjects(): Builder|BelongsToMany
{
if ($this->isAdmin()) {
return Project::query();
}
return $this->projects();
}
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
return $this->belongsToMany(Project::class, 'user_project')
->withTimestamps();
}
public function currentProject(): HasOne

View File

@ -0,0 +1,42 @@
<?php
namespace App\Policies;
use App\Models\PersonalAccessToken;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class PersonalAccessTokenPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->isAdmin();
}
public function view(User $user, PersonalAccessToken $personalAccessToken): bool
{
return $user->isAdmin();
}
public function create(User $user): bool
{
return $user->isAdmin();
}
public function update(User $user, PersonalAccessToken $personalAccessToken): bool
{
return $user->isAdmin();
}
public function delete(User $user, PersonalAccessToken $personalAccessToken): bool
{
return $user->isAdmin();
}
public function deleteMany(User $user): bool
{
return $user->isAdmin();
}
}

View File

@ -2,14 +2,15 @@
namespace App\Policies;
use App\Models\Project;
use App\Models\Server;
use App\Models\User;
class ServerPolicy
{
public function viewAny(User $user): bool
public function viewAny(User $user, Project $project): bool
{
return $user->isAdmin() || $user->currentProject?->users->contains($user);
return $user->isAdmin() || $project->users->contains($user);
}
public function view(User $user, Server $server): bool
@ -17,9 +18,9 @@ public function view(User $user, Server $server): bool
return $user->isAdmin() || $server->project->users->contains($user);
}
public function create(User $user): bool
public function create(User $user, Project $project): bool
{
return $user->isAdmin() || $user->currentProject?->users->contains($user);
return $user->isAdmin() || $project->users->contains($user);
}
public function update(User $user, Server $server): bool

View File

@ -12,26 +12,29 @@ class ServerProviderPolicy
public function viewAny(User $user): bool
{
return $user->isAdmin();
return true;
}
public function view(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
return $user->isAdmin() ||
$user->id === $serverProvider->user_id ||
$serverProvider->project_id === null ||
$serverProvider->project?->users()->where('user_id', $user->id)->exists();
}
public function create(User $user): bool
{
return $user->isAdmin();
return true;
}
public function update(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
return $user->isAdmin() || $user->id === $serverProvider->user_id;
}
public function delete(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
return $user->isAdmin() || $user->id === $serverProvider->user_id;
}
}

View File

@ -5,9 +5,11 @@
use App\Helpers\FTP;
use App\Helpers\Notifier;
use App\Helpers\SSH;
use App\Models\PersonalAccessToken;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
@ -33,5 +35,7 @@ public function boot(): void
$this->app->bind('ftp', function () {
return new FTP;
});
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}
}

View File

@ -6,7 +6,6 @@
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
@ -25,15 +24,6 @@ class RouteServiceProvider extends ServiceProvider
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
/**

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Http\Middleware\HasProjectMiddleware;
use App\Web\Pages\Settings\Projects\Widgets\SelectProject;
use Exception;
use Filament\Facades\Filament;
@ -102,6 +103,7 @@ public function panel(Panel $panel): Panel
])
->authMiddleware([
Authenticate::class,
HasProjectMiddleware::class,
])
->login()
->spa()

View File

@ -24,8 +24,6 @@ public function uninstall(): void
$this->getScript('supervisor/uninstall-supervisor.sh'),
'uninstall-supervisor'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}

View File

@ -21,11 +21,6 @@ public function createRules(array $input): array
{
return [
'token' => 'required',
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
];
}
@ -36,6 +31,16 @@ public function createData(array $input): array
];
}
public function editRules(array $input): array
{
return $this->createRules($input);
}
public function editData(array $input): array
{
return $this->createData($input);
}
public function data(): array
{
// support for older data

View File

@ -14,6 +14,18 @@ class Gitlab extends AbstractSourceControlProvider
protected string $apiVersion = 'api/v4';
public function createRules(array $input): array
{
return [
'token' => 'required',
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
];
}
public function connect(): bool
{
$res = Http::withToken($this->data()['token'])

View File

@ -8,6 +8,10 @@ public function createRules(array $input): array;
public function createData(array $input): array;
public function editRules(array $input): array;
public function editData(array $input): array;
public function data(): array;
public function connect(): bool;

View File

@ -38,12 +38,7 @@ function htmx(): HtmxResponse
function vito_version(): string
{
$version = exec('git describe --tags');
if (str($version)->contains('-')) {
return str($version)->before('-').' (dev)';
}
return $version;
return config('app.version');
}
function convert_time_format($string): string

13
app/Traits/Enum.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Traits;
trait Enum
{
public static function all(): array
{
$reflection = new \ReflectionClass(self::class);
return $reflection->getConstants();
}
}

View File

@ -34,7 +34,7 @@ public static function getNavigationItemActiveRoutePattern(): string
public function mount(): void
{
$this->authorize('viewAny', Server::class);
$this->authorize('viewAny', [Server::class, auth()->user()->currentProject]);
}
public function getWidgets(): array
@ -50,6 +50,8 @@ protected function getHeaderActions(): array
'public_key' => get_public_key_content(),
]);
$project = auth()->user()->currentProject;
return [
\Filament\Actions\Action::make('read-the-docs')
->label('Read the Docs')
@ -60,7 +62,7 @@ protected function getHeaderActions(): array
\Filament\Actions\Action::make('create')
->label('Create a Server')
->icon('heroicon-o-plus')
->authorize('create', Server::class)
->authorize('create', [Server::class, auth()->user()->currentProject])
->modalWidth(MaxWidth::FiveExtraLarge)
->slideOver()
->form([
@ -74,7 +76,7 @@ protected function getHeaderActions(): array
$set('region', null);
$set('plan', null);
})
->rules(fn ($get) => CreateServerAction::rules($get())['provider']),
->rules(fn ($get) => CreateServerAction::rules($project, $get())['provider']),
AlertField::make('alert')
->warning()
->message(__('servers.create.public_key_warning'))
@ -82,7 +84,7 @@ protected function getHeaderActions(): array
Select::make('server_provider')
->visible(fn ($get) => $get('provider') !== ServerProvider::CUSTOM)
->label('Server provider connection')
->rules(fn ($get) => CreateServerAction::rules($get())['server_provider'])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['server_provider'])
->options(function ($get) {
return \App\Models\ServerProvider::getByProjectId(auth()->user()->current_project_id)
->where('provider', $get('provider'))
@ -109,7 +111,7 @@ protected function getHeaderActions(): array
->schema([
Select::make('region')
->label('Region')
->rules(fn ($get) => CreateServerAction::rules($get())['region'])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['region'])
->live()
->reactive()
->options(function ($get) {
@ -125,7 +127,7 @@ protected function getHeaderActions(): array
->searchable(),
Select::make('plan')
->label('Plan')
->rules(fn ($get) => CreateServerAction::rules($get())['plan'])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['plan'])
->reactive()
->options(function ($get) {
if (! $get('server_provider') || ! $get('region')) {
@ -162,15 +164,15 @@ protected function getHeaderActions(): array
->visible(fn ($get) => $get('provider') === ServerProvider::CUSTOM),
TextInput::make('name')
->label('Name')
->rules(fn ($get) => CreateServerAction::rules($get())['name']),
->rules(fn ($get) => CreateServerAction::rules($project, $get())['name']),
Grid::make()
->schema([
TextInput::make('ip')
->label('SSH IP Address')
->rules(fn ($get) => CreateServerAction::rules($get())['ip']),
->rules(fn ($get) => CreateServerAction::rules($project, $get())['ip']),
TextInput::make('port')
->label('SSH Port')
->rules(fn ($get) => CreateServerAction::rules($get())['port']),
->rules(fn ($get) => CreateServerAction::rules($project, $get())['port']),
])
->visible(fn ($get) => $get('provider') === ServerProvider::CUSTOM),
Grid::make()
@ -178,7 +180,7 @@ protected function getHeaderActions(): array
Select::make('os')
->label('OS')
->native(false)
->rules(fn ($get) => CreateServerAction::rules($get())['os'])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['os'])
->options(
collect(config('core.operating_systems'))
->mapWithKeys(fn ($value) => [$value => $value])
@ -187,7 +189,7 @@ protected function getHeaderActions(): array
->label('Server Type')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($get())['type'])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['type'])
->options(
collect(config('core.server_types'))
->mapWithKeys(fn ($value) => [$value => $value])
@ -200,7 +202,7 @@ protected function getHeaderActions(): array
->label('Webserver')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($get())['webserver'] ?? [])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['webserver'] ?? [])
->options(
collect(config('core.webservers'))->mapWithKeys(fn ($value) => [$value => $value])
),
@ -208,7 +210,7 @@ protected function getHeaderActions(): array
->label('Database')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($get())['database'] ?? [])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['database'] ?? [])
->options(
collect(config('core.databases_name'))
->mapWithKeys(fn ($value, $key) => [
@ -219,7 +221,7 @@ protected function getHeaderActions(): array
->label('PHP')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($get())['php'] ?? [])
->rules(fn ($get) => CreateServerAction::rules($project, $get())['php'] ?? [])
->options(
collect(config('core.php_versions'))
->mapWithKeys(fn ($value) => [$value => $value])
@ -229,7 +231,7 @@ protected function getHeaderActions(): array
->modalSubmitActionLabel('Create')
->action(function (array $data) {
run_action($this, function () use ($data) {
$server = app(CreateServerAction::class)->create(auth()->user(), $data);
$server = app(CreateServerAction::class)->create(auth()->user(), auth()->user()->currentProject, $data);
$this->redirect(View::getUrl(['server' => $server]));
});

View File

@ -8,7 +8,6 @@
use Exception;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
@ -115,12 +114,10 @@ public function getTable(): Table
->color('danger')
->authorize(fn ($record) => auth()->user()->can('delete', $record)),
])
->bulkActions(
BulkActionGroup::make([
DeleteBulkAction::make()
->requiresConfirmation()
->authorize(auth()->user()->can('deleteMany', [ServerLog::class, $this->server])),
])
);
->bulkActions([
DeleteBulkAction::make()
->requiresConfirmation()
->authorize(auth()->user()->can('deleteMany', [ServerLog::class, $this->server])),
]);
}
}

View File

@ -21,7 +21,7 @@ class Settings extends Page
public function mount(): void
{
$this->authorize('update', $this->server);
$this->authorize('update', [$this->server, auth()->user()->currentProject]);
}
public function getWidgets(): array
@ -45,6 +45,7 @@ protected function getHeaderActions(): array
->requiresConfirmation()
->modalHeading('Delete Server')
->modalDescription('Once your server is deleted, all of its resources and data will be permanently deleted and can\'t be restored')
->authorize('delete', $this->server)
->action(function () {
try {
$this->server->delete();

View File

@ -18,7 +18,7 @@ class View extends Page
public function mount(): void
{
$this->authorize('view', $this->server);
$this->authorize('view', [$this->server, auth()->user()->currentProject]);
$this->previousStatus = $this->server->status;
}

View File

@ -0,0 +1,115 @@
<?php
namespace App\Web\Pages\Settings\APIKeys;
use App\Models\PersonalAccessToken;
use App\Web\Components\Page;
use Filament\Actions\Action;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
protected static ?string $navigationGroup = 'Settings';
protected static ?string $slug = 'settings/api-keys';
protected static ?string $title = 'API Keys';
protected static ?string $navigationIcon = 'icon-plug';
protected static ?int $navigationSort = 11;
public string $token = '';
protected $listeners = ['$refresh'];
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', PersonalAccessToken::class) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\ApiKeysList::class],
];
}
public function unmountAction(bool $shouldCancelParentActions = true, bool $shouldCloseModal = true): void
{
parent::unmountAction($shouldCancelParentActions, $shouldCloseModal);
$this->token = '';
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url(config('scribe.static.url'))
->openUrlInNewTab(),
Action::make('create')
->label('Create new Key')
->icon('heroicon-o-plus')
->modalHeading('Create a new Key')
->modalSubmitActionLabel('Create')
->form(function () {
if ($this->token) {
return [];
}
return [
TextInput::make('name')
->label('Token Name')
->required(),
Radio::make('permission')
->options([
'read' => 'Read',
'write' => 'Read & Write',
])
->required(),
];
})
->infolist(function () {
if ($this->token) {
return [
TextEntry::make('token')
->state($this->token)
->tooltip('Copy')
->copyable()
->helperText('You can see the token only one!'),
];
}
return [];
})
->authorize('create', PersonalAccessToken::class)
->modalWidth(MaxWidth::Large)
->action(function (array $data) {
$permissions = ['read'];
if ($data['permission'] === 'write') {
$permissions[] = 'write';
}
$token = auth()->user()->createToken($data['name'], $permissions);
$this->dispatch('$refresh');
$this->token = $token->plainTextToken;
$this->halt();
})
->modalSubmitAction(function () {
if ($this->token) {
return false;
}
})
->closeModalByClickingAway(fn () => ! $this->token),
];
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Web\Pages\Settings\APIKeys\Widgets;
use App\Models\PersonalAccessToken;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class ApiKeysList extends Widget
{
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return auth()->user()->tokens()->getQuery();
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('abilities')
->searchable()
->sortable(),
TextColumn::make('created_at')
->label('Created At')
->formatStateUsing(fn (PersonalAccessToken $record) => $record->created_at_by_timezone)
->searchable()
->sortable(),
TextColumn::make('last_used_at')
->label('Last Used At')
->formatStateUsing(fn (PersonalAccessToken $record) => $record->getDateTimeByTimezone($record->last_used_at))
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->heading('')
->actions([
DeleteAction::make('delete')
->modalHeading('Delete Token')
->authorize(fn (PersonalAccessToken $record) => auth()->user()->can('delete', $record))
->using(function (array $data, PersonalAccessToken $record) {
$record->delete();
}),
])
->bulkActions([
DeleteBulkAction::make()
->requiresConfirmation()
->authorize(auth()->user()->can('deleteMany', PersonalAccessToken::class)),
]);
}
}

View File

@ -15,9 +15,7 @@
class NotificationChannelsList extends Widget
{
protected $listeners = [
'$refresh' => 'refreshTable',
];
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{

View File

@ -4,7 +4,6 @@
use App\Models\Project;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class SelectProject extends Widget
@ -20,15 +19,7 @@ class SelectProject extends Widget
public function mount(): void
{
$this->currentProject = auth()->user()->currentProject;
$this->projects = Project::query()
->where(function (Builder $query) {
if (auth()->user()->isAdmin()) {
return;
}
$query->where('user_id', auth()->id())
->orWhereHas('users', fn ($query) => $query->where('user_id', auth()->id()));
})
->get();
$this->projects = auth()->user()->allProjects()->get();
}
public function updateProject(Project $project): void

View File

@ -8,6 +8,7 @@
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
class Create
@ -23,29 +24,22 @@ public static function form(): array
)
->live()
->reactive()
->rules(CreateServerProvider::rules()['provider']),
->rules(fn (Get $get) => CreateServerProvider::rules($get())['provider']),
TextInput::make('name')
->rules(CreateServerProvider::rules()['name']),
->rules(fn (Get $get) => CreateServerProvider::rules($get())['name']),
TextInput::make('token')
->label('API Key')
->validationAttribute('API Key')
->visible(fn ($get) => in_array($get('provider'), [
ServerProvider::DIGITALOCEAN,
ServerProvider::LINODE,
ServerProvider::VULTR,
ServerProvider::HETZNER,
]))
->rules(fn ($get) => CreateServerProvider::providerRules($get())['token']),
->visible(fn ($get) => isset(CreateServerProvider::rules($get())['token']))
->rules(fn (Get $get) => CreateServerProvider::rules($get())['token']),
TextInput::make('key')
->label('Access Key')
->visible(function ($get) {
return $get('provider') == ServerProvider::AWS;
})
->rules(fn ($get) => CreateServerProvider::providerRules($get())['key']),
->visible(fn ($get) => isset(CreateServerProvider::rules($get())['key']))
->rules(fn (Get $get) => CreateServerProvider::rules($get())['key']),
TextInput::make('secret')
->label('Secret')
->visible(fn ($get) => $get('provider') == ServerProvider::AWS)
->rules(fn ($get) => CreateServerProvider::providerRules($get())['secret']),
->visible(fn ($get) => isset(CreateServerProvider::rules($get())['secret']))
->rules(fn (Get $get) => CreateServerProvider::rules($get())['secret']),
Checkbox::make('global')
->label('Is Global (Accessible in all projects)'),
];
@ -57,7 +51,7 @@ public static function form(): array
public static function action(array $data): void
{
try {
app(CreateServerProvider::class)->create(auth()->user(), $data);
app(CreateServerProvider::class)->create(auth()->user(), auth()->user()->currentProject, $data);
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())

View File

@ -22,6 +22,6 @@ public static function form(): array
public static function action(ServerProvider $provider, array $data): void
{
app(EditServerProvider::class)->edit($provider, auth()->user(), $data);
app(EditServerProvider::class)->edit($provider, auth()->user()->currentProject, $data);
}
}

View File

@ -56,7 +56,7 @@ public static function form(): array
public static function action(array $data): void
{
try {
app(ConnectSourceControl::class)->connect(auth()->user(), $data);
app(ConnectSourceControl::class)->connect(auth()->user(), auth()->user()->currentProject, $data);
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())

View File

@ -2,9 +2,8 @@
namespace App\Web\Pages\Settings\SourceControls\Actions;
use App\Actions\SourceControl\ConnectSourceControl;
use App\Actions\SourceControl\EditSourceControl;
use App\Enums\SourceControl;
use App\Models\SourceControl;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
@ -13,30 +12,27 @@
class Edit
{
public static function form(): array
public static function form(SourceControl $sourceControl): array
{
return [
TextInput::make('name')
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['name']),
->rules(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['name']),
TextInput::make('token')
->label('API Key')
->validationAttribute('API Key')
->visible(fn ($get) => in_array($get('provider'), [
SourceControl::GITHUB,
SourceControl::GITLAB,
]))
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['token']),
->visible(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['token'] ?? false)
->rules(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['token']),
TextInput::make('url')
->label('URL (optional)')
->visible(fn ($get) => $get('provider') == SourceControl::GITLAB)
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['url'])
->visible(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['url'] ?? false)
->rules(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['url'])
->helperText('If you run a self-managed gitlab enter the url here, leave empty to use gitlab.com'),
TextInput::make('username')
->visible(fn ($get) => $get('provider') == SourceControl::BITBUCKET)
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['username']),
->visible(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['username'] ?? false)
->rules(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['username']),
TextInput::make('password')
->visible(fn ($get) => $get('provider') == SourceControl::BITBUCKET)
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['password']),
->visible(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['password'] ?? false)
->rules(fn (Get $get) => EditSourceControl::rules($sourceControl, $get())['password']),
Checkbox::make('global')
->label('Is Global (Accessible in all projects)'),
];
@ -45,10 +41,10 @@ public static function form(): array
/**
* @throws Exception
*/
public static function action(\App\Models\SourceControl $sourceControl, array $data): void
public static function action(SourceControl $sourceControl, array $data): void
{
try {
app(EditSourceControl::class)->edit($sourceControl, auth()->user(), $data);
app(EditSourceControl::class)->edit($sourceControl, auth()->user()->currentProject, $data);
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())

View File

@ -69,7 +69,7 @@ public function getTable(): Table
'global' => $record->project_id === null,
];
})
->form(Edit::form())
->form(fn (SourceControl $record) => Edit::form($record))
->authorize(fn (SourceControl $record) => auth()->user()->can('update', $record))
->using(fn (array $data, SourceControl $record) => Edit::action($record, $data))
->modalWidth(MaxWidth::Medium),

View File

@ -126,7 +126,7 @@ public static function form(): array
public static function action(array $data): void
{
try {
app(CreateStorageProvider::class)->create(auth()->user(), $data);
app(CreateStorageProvider::class)->create(auth()->user(), auth()->user()->currentProject, $data);
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())

View File

@ -22,6 +22,6 @@ public static function form(): array
public static function action(StorageProvider $provider, array $data): void
{
app(EditStorageProvider::class)->edit($provider, auth()->user(), $data);
app(EditStorageProvider::class)->edit($provider, auth()->user()->currentProject, $data);
}
}