diff --git a/.editorconfig b/.editorconfig index 646b8213..f58e90a8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,12 +3,15 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_size = 4 +indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ij_any_block_comment_at_first_column = false +[*.php] +indent_size = 4 + [*.md] trim_trailing_whitespace = false diff --git a/.prettierignore b/.prettierignore index 46ea115a..47061dda 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,6 @@ sail !*.blade.php !*.sh resources/views/ssh/ +resources/views/scribe/ +resources/js/ziggy.js +resources/views/mail/* diff --git a/.prettierrc b/.prettierrc index 3c78153f..88a8814d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,26 +1,46 @@ { - "plugins": [ - "prettier-plugin-blade", - "prettier-plugin-tailwindcss", - "prettier-plugin-sh" - ], - "overrides": [ - { - "files": ["*.blade.php"], - "options": { - "parser": "blade", - "printWidth": 120, - "htmlWhitespaceSensitivity": "ignore", - "tabWidth": 4, - "quoteProps": "consistent", - "trailingComma": "none" - } - }, - { - "files": ["*.sh"], - "options": { - "parser": "sh" - } - } - ] + "semi": true, + "singleQuote": true, + "singleAttributePerLine": false, + "htmlWhitespaceSensitivity": "css", + "printWidth": 150, + "tailwindFunctions": [ + "clsx", + "cn" + ], + "tabWidth": 2, + "plugins": [ + "prettier-plugin-blade", + "prettier-plugin-tailwindcss", + "prettier-plugin-sh" + ], + "overrides": [ + { + "files": [ + "*.blade.php" + ], + "options": { + "parser": "blade", + "printWidth": 120, + "htmlWhitespaceSensitivity": "ignore", + "tabWidth": 4, + "quoteProps": "consistent", + "trailingComma": "none" + } + }, + { + "files": [ + "*.sh" + ], + "options": { + "parser": "sh" + } + }, + { + "files": "**/*.yml", + "options": { + "tabWidth": 2 + } + } + ] } diff --git a/README.md b/README.md index 7fa8a186..1321ec26 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,6 @@ ## Credits - PHPSecLib - PHPUnit - Tailwindcss -- Alpinejs -- Livewire - Vite - Prettier -- Postcss -- FilamentPHP -- Mobiledetect - Spatie diff --git a/app/Actions/Server/CreateServer.php b/app/Actions/Server/CreateServer.php index eebd2a3b..0af7d88f 100755 --- a/app/Actions/Server/CreateServer.php +++ b/app/Actions/Server/CreateServer.php @@ -17,6 +17,7 @@ use Exception; use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -29,6 +30,8 @@ class CreateServer */ public function create(User $creator, Project $project, array $input): Server { + Validator::make($input, self::rules($project, $input))->validate(); + $server = new Server([ 'project_id' => $project->id, 'user_id' => $creator->id, diff --git a/app/Actions/ServerProvider/CreateServerProvider.php b/app/Actions/ServerProvider/CreateServerProvider.php index 71a80c60..893e5dcc 100644 --- a/app/Actions/ServerProvider/CreateServerProvider.php +++ b/app/Actions/ServerProvider/CreateServerProvider.php @@ -8,6 +8,7 @@ use App\Models\User; use App\ServerProviders\ServerProvider as ServerProviderContract; use Exception; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -20,6 +21,8 @@ class CreateServerProvider */ public function create(User $user, Project $project, array $input): ServerProvider { + Validator::make($input, self::rules($input))->validate(); + $provider = self::getProvider($input['provider']); try { diff --git a/app/Facades/SSH.php b/app/Facades/SSH.php index 2c0c57be..b58c4c31 100644 --- a/app/Facades/SSH.php +++ b/app/Facades/SSH.php @@ -15,7 +15,7 @@ * @method static string upload(string $local, string $remote, ?string $owner = null) * @method static string download(string $local, string $remote) * @method static string write(string $path, string $content, string $owner = null) - * @method static string assertExecuted(array|string $commands) + * @method static string assertExecuted(mixed $commands) * @method static string assertExecutedContains(string $command) * @method static string assertFileUploaded(string $toPath, ?string $content = null) * @method static string getUploadedLocalPath() diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 00000000..a73c066f --- /dev/null +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,88 @@ + Route::has('password.request'), + 'status' => $request->session()->get('status'), + ]); + } + + #[Post('login', name: 'login', middleware: 'guest')] + public function store(Request $request): RedirectResponse + { + $this->validate($request, [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]); + + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt(['email' => $request->email, 'password' => $request->password], $request->remember)) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + Session::regenerate(); + + return redirect()->intended(route('servers', absolute: false)); + } + + #[Post('logout', name: 'logout', middleware: 'auth')] + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } + + protected function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout(request())); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + protected function throttleKey(): string + { + return Str::transliterate(Str::lower(request()->email).'|'.request()->ip()); + } +} diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 00000000..81375f21 --- /dev/null +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,43 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(route('dashboard', absolute: false)); + } +} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 00000000..289d9296 --- /dev/null +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,69 @@ + $request->email, + 'token' => $request->route('token'), + ]); + } + + #[Post('/', name: 'password.store')] + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status == Password::PasswordReset) { + return to_route('login')->with('status', __($status)); + } + + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 00000000..57d52df5 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,41 @@ + $request->session()->get('status'), + ]); + } + + #[Post('/', name: 'password.email')] + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'email' => 'required|email', + ]); + + Password::sendResetLink( + $request->only('email') + ); + + return back()->with('status', __('A reset link will be sent if the account exists.')); + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php new file mode 100644 index 00000000..97ad73c9 --- /dev/null +++ b/app/Http/Controllers/HomeController.php @@ -0,0 +1,19 @@ +check()) { + return redirect()->route('servers'); + } + + return redirect()->route('login'); + } +} diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php new file mode 100644 index 00000000..ade783c0 --- /dev/null +++ b/app/Http/Controllers/ServerController.php @@ -0,0 +1,70 @@ +currentProject; + + $this->authorize('viewAny', [Server::class, $project]); + + $servers = $project->servers()->simplePaginate(config('web.pagination_size')); + + return inertia('servers/index', [ + 'servers' => ServerResource::collection($servers), + 'public_key' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]), + 'server_providers' => ServerProviderResource::collection(ServerProvider::getByProjectId($project->id)->get()), + ]); + } + + #[Post('/', name: 'servers.store')] + public function store(Request $request): RedirectResponse + { + $project = user()->currentProject; + + $this->authorize('create', [Server::class, $project]); + + $server = app(CreateServer::class)->create(user(), $project, $request->all()); + + return redirect()->route('servers.show', ['server' => $server->id]); + } + + #[Get('/{server}', name: 'servers.show')] + public function show(Server $server): Response|ResponseFactory + { + $this->authorize('view', $server); + + return inertia('servers/show', [ + 'server' => ServerResource::make($server), + 'logs' => ServerLogResource::collection($server->logs()->latest()->paginate(config('web.pagination_size'))), + ]); + } + + #[Post('/{server}/switch', name: 'servers.switch')] + public function switch(Server $server): RedirectResponse + { + $this->authorize('view', $server); + + return redirect()->route('servers.show', ['server' => $server->id]); + } +} diff --git a/app/Http/Controllers/ServerLogController.php b/app/Http/Controllers/ServerLogController.php new file mode 100644 index 00000000..5c7e77a3 --- /dev/null +++ b/app/Http/Controllers/ServerLogController.php @@ -0,0 +1,22 @@ +authorize('view', $log); + + return $log->getContent(); + } +} diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php new file mode 100644 index 00000000..909b267a --- /dev/null +++ b/app/Http/Controllers/Settings/PasswordController.php @@ -0,0 +1,41 @@ +validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back(); + } +} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php new file mode 100644 index 00000000..09d78d22 --- /dev/null +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -0,0 +1,73 @@ + $request->user() instanceof MustVerifyEmail, + 'status' => $request->session()->get('status'), + ]); + } + + #[Patch('/', name: 'profile.update')] + public function update(Request $request): RedirectResponse + { + $this->validate($request, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore(user()->id), + ], + ]); + $request->user()->fill($request->only('name', 'email')); + + $request->user()->save(); + + return to_route('profile.edit'); + } + + #[Delete('/', name: 'profile.destroy')] + public function destroy(Request $request): RedirectResponse + { + $request->validate([ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Settings/ProjectController.php b/app/Http/Controllers/Settings/ProjectController.php new file mode 100644 index 00000000..333d0348 --- /dev/null +++ b/app/Http/Controllers/Settings/ProjectController.php @@ -0,0 +1,27 @@ +authorize('view', $project); + + user()->update([ + 'current_project_id' => $project->id, + ]); + + return redirect()->route('servers'); + } +} diff --git a/app/Http/Controllers/Settings/ServerProviderController.php b/app/Http/Controllers/Settings/ServerProviderController.php new file mode 100644 index 00000000..50900294 --- /dev/null +++ b/app/Http/Controllers/Settings/ServerProviderController.php @@ -0,0 +1,57 @@ +authorize('viewAny', ServerProvider::class); + + return ServerProviderResource::collection(ServerProvider::getByProjectId(user()->current_project_id)->get()); + } + + #[Post('/', name: 'server-providers.store')] + public function store(Request $request): RedirectResponse + { + $this->authorize('create', ServerProvider::class); + + app(CreateServerProvider::class)->create(user(), user()->currentProject, $request->all()); + + return back()->with('success', 'Server provider created.'); + } + + #[Get('/{serverProvider}/regions', name: 'server-providers.regions')] + public function regions(ServerProvider $serverProvider): JsonResponse + { + $this->authorize('view', $serverProvider); + + return response()->json($serverProvider->provider()->regions()); + } + + #[Get('{serverProvider}/regions/{region}/plans', name: 'server-providers.plans')] + public function plans(ServerProvider $serverProvider, string $region): JsonResponse + { + $this->authorize('view', $serverProvider); + + return response()->json($serverProvider->provider()->plans($region)); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 4d3e196b..5bcea704 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,6 +36,9 @@ class Kernel extends HttpKernel \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\HandleInertiaRequests::class, + \App\Http\Middleware\HandleAppearance::class, + \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, ], 'api' => [ diff --git a/app/Http/Middleware/CanSeeProjectMiddleware.php b/app/Http/Middleware/CanSeeProjectMiddleware.php index 9806099c..1bdfacc7 100644 --- a/app/Http/Middleware/CanSeeProjectMiddleware.php +++ b/app/Http/Middleware/CanSeeProjectMiddleware.php @@ -9,10 +9,7 @@ class CanSeeProjectMiddleware { - /** - * @return mixed - */ - public function handle(Request $request, Closure $next) + public function handle(Request $request, Closure $next): mixed { /** @var User $user */ $user = $request->user(); diff --git a/app/Http/Middleware/HandleAppearance.php b/app/Http/Middleware/HandleAppearance.php new file mode 100644 index 00000000..1a01684d --- /dev/null +++ b/app/Http/Middleware/HandleAppearance.php @@ -0,0 +1,23 @@ +cookie('appearance') ?? 'system'); + + return $next($request); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 00000000..149359a4 --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,73 @@ + + */ + public function share(Request $request): array + { + [$message, $author] = str(Inspiring::quotes()->random())->explode('-'); + + /** @var ?User $user */ + $user = $request->user(); + + // servers + $servers = []; + if ($user && $user->can('viewAny', [Server::class, $user->currentProject])) { + $servers = ServerResource::collection($user->currentProject?->servers); + } + + return [ + ...parent::share($request), + 'name' => config('app.name'), + 'quote' => ['message' => trim($message), 'author' => trim($author)], + 'auth' => [ + 'user' => $user, + 'projects' => $user?->allProjects()->get(), + 'currentProject' => $user?->currentProject, + ], + 'publicKeyText' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]), + 'projectServers' => $servers, + 'configs' => config('core'), + 'ziggy' => fn (): array => [ + ...(new Ziggy)->toArray(), + 'location' => $request->url(), + ], + 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', + ]; + } +} diff --git a/app/Http/Middleware/HasProjectMiddleware.php b/app/Http/Middleware/HasProjectMiddleware.php index 9ec09dfd..667937ef 100644 --- a/app/Http/Middleware/HasProjectMiddleware.php +++ b/app/Http/Middleware/HasProjectMiddleware.php @@ -2,16 +2,14 @@ namespace App\Http\Middleware; +use App\Models\Project; use App\Models\User; use Closure; use Illuminate\Http\Request; class HasProjectMiddleware { - /** - * @return mixed - */ - public function handle(Request $request, Closure $next) + public function handle(Request $request, Closure $next): mixed { /** @var ?User $user */ $user = $request->user(); @@ -21,7 +19,7 @@ public function handle(Request $request, Closure $next) if (! $user->currentProject) { if ($user->allProjects()->count() > 0) { - /** @var \App\Models\Project $firstProject */ + /** @var Project $firstProject */ $firstProject = $user->allProjects()->first(); $user->current_project_id = $firstProject->id; $user->save(); diff --git a/app/Http/Resources/ServerLogResource.php b/app/Http/Resources/ServerLogResource.php new file mode 100644 index 00000000..742bd31f --- /dev/null +++ b/app/Http/Resources/ServerLogResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'server_id' => $this->server_id, + 'site_id' => $this->site_id, + 'type' => $this->type, + 'name' => $this->name, + 'disk' => $this->disk, + 'is_remote' => $this->is_remote, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'created_at_by_timezone' => $this->created_at_by_timezone, + 'updated_at_by_timezone' => $this->updated_at_by_timezone, + ]; + } +} diff --git a/app/Http/Resources/ServerProviderResource.php b/app/Http/Resources/ServerProviderResource.php index 22e25094..d85f58bc 100644 --- a/app/Http/Resources/ServerProviderResource.php +++ b/app/Http/Resources/ServerProviderResource.php @@ -22,6 +22,8 @@ public function toArray(Request $request): array 'provider' => $this->provider, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, + 'created_at_by_timezone' => $this->created_at_by_timezone, + 'updated_at_by_timezone' => $this->updated_at_by_timezone, ]; } } diff --git a/app/Http/Resources/ServerResource.php b/app/Http/Resources/ServerResource.php index 06f536fe..5983b68f 100644 --- a/app/Http/Resources/ServerResource.php +++ b/app/Http/Resources/ServerResource.php @@ -38,8 +38,11 @@ public function toArray(Request $request): array 'progress_step' => $this->progress_step, 'updates' => $this->updates, 'last_update_check' => $this->last_update_check, + 'status_color' => $this->getStatusColor(), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, + 'created_at_by_timezone' => $this->created_at_by_timezone, + 'updated_at_by_timezone' => $this->updated_at_by_timezone, ]; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index e9d82c45..4f1a64ce 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -13,6 +13,7 @@ use App\SSH\Systemd\Systemd; use App\Support\Testing\SSHFake; use Carbon\Carbon; +use Database\Factories\ServerFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -68,7 +69,7 @@ */ class Server extends AbstractModel { - /** @use HasFactory<\Database\Factories\ServerFactory> */ + /** @use HasFactory */ use HasFactory; protected $fillable = [ @@ -570,4 +571,13 @@ public function download(string $path, string $disk = 'tmp'): void $path ); } + + public function getStatusColor(): string + { + if (isset(self::$statusColors[$this->status])) { + return self::$statusColors[$this->status]; + } + + return 'gray'; + } } diff --git a/app/Models/User.php b/app/Models/User.php index d270311d..864619e2 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,8 +5,7 @@ use App\Enums\UserRole; use App\Traits\HasTimezoneTimestamps; use Carbon\Carbon; -use Filament\Models\Contracts\FilamentUser; -use Filament\Panel; +use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -43,11 +42,11 @@ * @property Carbon $created_at * @property Carbon $updated_at */ -class User extends Authenticatable implements FilamentUser +class User extends Authenticatable { use HasApiTokens; - /** @use HasFactory<\Database\Factories\UserFactory> */ + /** @use HasFactory */ use HasFactory; use HasTimezoneTimestamps; @@ -70,8 +69,7 @@ class User extends Authenticatable implements FilamentUser 'two_factor_secret', ]; - protected $appends = [ - ]; + protected $appends = []; /** * @return HasMany @@ -204,9 +202,4 @@ public function allServers(): Builder }); }); } - - public function canAccessPanel(Panel $panel): bool - { - return true; - } } diff --git a/app/NotificationChannels/Discord.php b/app/NotificationChannels/Discord.php index 5a4cf7a6..be4197de 100644 --- a/app/NotificationChannels/Discord.php +++ b/app/NotificationChannels/Discord.php @@ -4,7 +4,6 @@ use App\Models\NotificationChannel; use App\Notifications\NotificationInterface; -use App\Web\Pages\Settings\NotificationChannels\Index; use Illuminate\Support\Facades\Http; class Discord extends AbstractNotificationChannel @@ -39,7 +38,7 @@ public function connect(): bool __('Congratulations! 🎉'), __("You've connected your Discord to :app", ['app' => config('app.name')])."\n". __('Manage your notification channels')."\n". - Index::getUrl() + '/settings/notification-channels', ); if (! $connect) { diff --git a/app/NotificationChannels/Slack.php b/app/NotificationChannels/Slack.php index 353beb61..4f7763cd 100644 --- a/app/NotificationChannels/Slack.php +++ b/app/NotificationChannels/Slack.php @@ -4,7 +4,6 @@ use App\Models\NotificationChannel; use App\Notifications\NotificationInterface; -use App\Web\Pages\Settings\NotificationChannels\Index; use Illuminate\Support\Facades\Http; class Slack extends AbstractNotificationChannel @@ -39,7 +38,7 @@ public function connect(): bool __('Congratulations! 🎉'), __("You've connected your Slack to :app", ['app' => config('app.name')])."\n". __('Manage your notification channels')."\n". - Index::getUrl() + '/settings/notification-channels', ); if (! $connect) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0d8cccf0..1b39446c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -26,9 +26,9 @@ public function boot(): void ResourceCollection::withoutWrapping(); // facades - $this->app->bind('ssh', fn (): \App\Helpers\SSH => new SSH); - $this->app->bind('notifier', fn (): \App\Helpers\Notifier => new Notifier); - $this->app->bind('ftp', fn (): \App\Helpers\FTP => new FTP); + $this->app->bind('ssh', fn (): SSH => new SSH); + $this->app->bind('notifier', fn (): Notifier => new Notifier); + $this->app->bind('ftp', fn (): FTP => new FTP); Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); } diff --git a/app/Providers/WebServiceProvider.php b/app/Providers/WebServiceProvider.php deleted file mode 100644 index d6619833..00000000 --- a/app/Providers/WebServiceProvider.php +++ /dev/null @@ -1,123 +0,0 @@ -panel(Panel::make())); - } - - public function boot(): void - { - FilamentView::registerRenderHook( - PanelsRenderHook::SIDEBAR_NAV_START, - fn () => Livewire::mount(SelectProject::class) - ); - FilamentView::registerRenderHook( - PanelsRenderHook::SIDEBAR_FOOTER, - fn () => view('components.app-version') - ); - FilamentAsset::register([ - Js::make('app', Vite::asset('resources/js/app.js'))->module(), - ]); - FilamentColor::register([ - 'slate' => Color::Slate, - 'gray' => Color::Zinc, - 'red' => Color::Red, - 'orange' => Color::Orange, - 'amber' => Color::Amber, - 'yellow' => Color::Yellow, - 'lime' => Color::Lime, - 'green' => Color::Green, - 'emerald' => Color::Emerald, - 'teal' => Color::Teal, - 'cyan' => Color::Cyan, - 'sky' => Color::Sky, - 'blue' => Color::Blue, - 'indigo' => Color::Indigo, - 'violet' => Color::Violet, - 'purple' => Color::Purple, - 'fuchsia' => Color::Fuchsia, - 'pink' => Color::Pink, - 'rose' => Color::Rose, - ]); - } - - /** - * @throws Exception - */ - public function panel(Panel $panel): Panel - { - return $panel - ->default() - ->id('app') - ->path('') - ->passwordReset() - ->colors([ - 'primary' => Color::Indigo, - ]) - ->maxContentWidth(MaxWidth::ScreenTwoExtraLarge) - ->viteTheme('resources/css/filament/app/theme.css') - ->brandLogo(fn () => view('components.brand')) - ->brandLogoHeight('30px') - ->discoverPages(in: app_path('Web/Pages'), for: 'App\\Web\\Pages') - ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - ]) - ->authMiddleware([ - Authenticate::class, - HasProjectMiddleware::class, - ]) - ->userMenuItems([ - 'profile' => MenuItem::make() - ->url(fn (): string => Profile\Index::getUrl()), - ]) - ->login(Login::class) - ->spa() - ->globalSearchKeyBindings(['command+k', 'ctrl+k']) - ->sidebarCollapsibleOnDesktop() - ->globalSearchFieldKeyBindingSuffix(); - } -} diff --git a/app/Support/helpers.php b/app/Support/helpers.php index 395f7f5b..1f203a9d 100755 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -1,12 +1,9 @@ has('ssh_public_key_content')) { + return cache()->get('ssh_public_key_content'); + } + if (! file_exists(storage_path(config('core.ssh_public_key_name')))) { Artisan::call('ssh-key:generate --force'); } @@ -67,46 +68,13 @@ function get_public_key_content(): string return ''; } - return str($content) + $content = str($content) ->replace("\n", '') ->toString(); -} -function run_action(object $static, Closure $callback): void -{ - try { - $callback(); - } catch (SSHError $e) { - $actions = []; - if ($e->getLog() instanceof \App\Models\ServerLog) { - $actions[] = Action::make('View Logs') - ->url(App\Web\Pages\Servers\Logs\Index::getUrl([ - 'server' => $e->getLog()->server_id, - ])) - ->openUrlInNewTab(); - } - Notification::make() - ->danger() - ->title($e->getMessage()) - ->body($e->getLog()?->getContent(30)) - ->actions($actions) - ->send(); + cache()->put('ssh_public_key_content', $content, 60 * 60 * 24); - if (method_exists($static, 'halt')) { - $reflectionMethod = new ReflectionMethod($static, 'halt'); - $reflectionMethod->invoke($static); - } - } catch (ValidationException $e) { - Notification::make() - ->danger() - ->title($e->getMessage()) - ->send(); - - if (method_exists($static, 'halt')) { - $reflectionMethod = new ReflectionMethod($static, 'halt'); - $reflectionMethod->invoke($static); - } - } + return $content; } /** @@ -251,3 +219,11 @@ function format_nginx_config(string $config): string return implode("\n", $formattedLines)."\n"; } + +function user(): User +{ + /** @var User $user */ + $user = auth()->user(); + + return $user; +} diff --git a/app/Web/Components/Link.php b/app/Web/Components/Link.php deleted file mode 100644 index dd311501..00000000 --- a/app/Web/Components/Link.php +++ /dev/null @@ -1,26 +0,0 @@ -render()->with([ - 'href' => $this->href, - 'text' => $this->text, - 'external' => $this->external, - ]); - } -} diff --git a/app/Web/Components/Page.php b/app/Web/Components/Page.php deleted file mode 100644 index 1d158aed..00000000 --- a/app/Web/Components/Page.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ - protected array $extraAttributes = []; - - /** - * @return array - */ - protected function getExtraAttributes(): array - { - $attributes = $this->extraAttributes; - - if (! in_array($this->getLive(), [null, '', '0'], true)) { - $attributes['wire:poll.'.$this->getLive()] = '$dispatch(\'$refresh\')'; - } - - return $attributes; - } - - public function getExtraAttributesBag(): ComponentAttributeBag - { - return new ComponentAttributeBag($this->getExtraAttributes()); - } - - public function getLive(): ?string - { - return $this->live; - } - - /** - * @return array - */ - public function getWidgets(): array - { - return []; - } - - protected function getUser(): User - { - /** @var User $user */ - $user = auth()->user(); - - return $user; - } -} diff --git a/app/Web/Contracts/HasSecondSubNav.php b/app/Web/Contracts/HasSecondSubNav.php deleted file mode 100644 index a5e58243..00000000 --- a/app/Web/Contracts/HasSecondSubNav.php +++ /dev/null @@ -1,11 +0,0 @@ - - */ - public function getSecondSubNavigation(): array; -} diff --git a/app/Web/Fields/AlertField.php b/app/Web/Fields/AlertField.php deleted file mode 100644 index 757b3980..00000000 --- a/app/Web/Fields/AlertField.php +++ /dev/null @@ -1,72 +0,0 @@ -color = $color; - - return $this; - } - - public function icon(string $icon): static - { - $this->icon = $icon; - - return $this; - } - - public function message(string $message): static - { - $this->message = $message; - - return $this; - } - - public function success(): static - { - return $this->color('green')->icon('heroicon-o-check-circle'); - } - - public function warning(): static - { - return $this->color('yellow')->icon('heroicon-o-exclamation-circle'); - } - - public function danger(): static - { - return $this->color('red')->icon('heroicon-o-x-circle'); - } - - public function info(): static - { - return $this->color('blue')->icon('heroicon-o-information-circle'); - } - - public function getColor(): string - { - return $this->color; - } - - public function getIcon(): string - { - return $this->icon; - } - - public function getMessage(): string - { - return $this->message; - } -} diff --git a/app/Web/Fields/CodeEditorField.php b/app/Web/Fields/CodeEditorField.php deleted file mode 100644 index cd4755d4..00000000 --- a/app/Web/Fields/CodeEditorField.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ - public function getOptions(): array - { - return [ - 'id' => $this->getId(), - 'name' => $this->getName(), - 'lang' => $this->lang, - 'value' => json_encode($this->getState() ?? ''), - ]; - } -} diff --git a/app/Web/Fields/ProviderField.php b/app/Web/Fields/ProviderField.php deleted file mode 100644 index 6fc3d8c2..00000000 --- a/app/Web/Fields/ProviderField.php +++ /dev/null @@ -1,10 +0,0 @@ -check()) { - redirect()->intended(Filament::getUrl()); - } - - $this->initTwoFactor(); - - $this->form->fill(); - - if (config('app.demo')) { - $this->form->fill([ - 'email' => 'demo@vitodeploy.com', - 'password' => 'password', - ]); - } - } - - public function logoutAction(): Action - { - return Action::make('logout') - ->label('Logout') - ->color('danger') - ->link() - ->action(function (): void { - Filament::auth()->logout(); - - session()->forget('login.id'); - - $this->redirect(Filament::getLoginUrl()); - }); - } - - protected function getForms(): array - { - if (request()->session()->has('login.id')) { - return [ - 'form' => $this->form( - $this->makeForm() - ->schema([ - TextInput::make('code') - ->label('2FA Code') - ->autofocus(), - TextInput::make('recovery_code') - ->label('Recovery Code') - ->autofocus(), - ]) - ->statePath('data'), - ), - ]; - } - - return parent::getForms(); - } - - public function authenticate(): ?LoginResponse - { - if (request()->session()->has('login.id')) { - return $this->confirmTwoFactor(); - } - - $loginResponse = parent::authenticate(); - - /** @var ?User $user */ - $user = Filament::auth()->getUser(); - if ($user && $user->two_factor_secret) { - Filament::auth()->logout(); - - request()->session()->put([ - 'login.id' => $user->getKey(), - 'login.remember' => $this->data['remember'] ?? false, - ]); - - $this->redirect(Filament::getLoginUrl()); - - return null; - } - - return $loginResponse; - } - - private function confirmTwoFactor(): LoginResponse - { - $request = TwoFactorLoginRequest::createFrom(request())->merge([ - 'code' => $this->data['code'] ?? null, - 'recovery_code' => $this->data['recovery_code'] ?? null, - ]); - - /** @var ?User $user */ - $user = $request->challengedUser(); - - if (! $user) { - $this->redirect(Filament::getLoginUrl()); - - return app(LoginResponse::class); - } - - if ($code = $request->validRecoveryCode()) { - $user->replaceRecoveryCode($code); - } elseif (! $request->hasValidCode()) { - $field = $request->input('recovery_code') ? 'recovery_code' : 'code'; - - $this->initTwoFactor(); - - throw ValidationException::withMessages([ - 'data.'.$field => 'Invalid code!', - ]); - } - - Filament::auth()->login($user, $request->remember()); - - return app(LoginResponse::class); - } - - protected function getAuthenticateFormAction(): Action - { - if (request()->session()->has('login.id')) { - return Action::make('verify') - ->label('Verify') - ->submit('authenticate'); - } - - return parent::getAuthenticateFormAction(); - } - - private function initTwoFactor(): void - { - if (request()->session()->has('login.id')) { - FilamentView::registerRenderHook( - PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE, - fn (): string => Blade::render( - <<{$this->logoutAction()->render()->render()} - BLADE - ), - ); - } - } -} diff --git a/app/Web/Pages/Scripts/Executions.php b/app/Web/Pages/Scripts/Executions.php deleted file mode 100644 index 5a9e5935..00000000 --- a/app/Web/Pages/Scripts/Executions.php +++ /dev/null @@ -1,83 +0,0 @@ -script->name.' - Executions'; - } - - public function mount(): void - { - $this->authorize('view', $this->script); - } - - public function getWidgets(): array - { - return [ - [Widgets\ScriptExecutionsList::class, ['script' => $this->script]], - ]; - } - - protected function getHeaderActions(): array - { - $form = [ - Select::make('server') - ->options(fn () => auth()->user()?->currentProject?->servers?->pluck('name', 'id') ?? []) - ->rules(fn (Get $get) => ExecuteScript::rules($get())['server']) - ->searchable() - ->reactive(), - Select::make('user') - ->rules(fn (Get $get) => ExecuteScript::rules($get())['user']) - ->native(false) - ->options(function (Get $get): array { - $users = ['root']; - - /** @var ?Server $server */ - $server = Server::query()->find($get('server')); - if ($server) { - $users = $server->getSshUsers(); - } - - return array_combine($users, $users); - }), - ]; - - foreach ($this->script->getVariables() as $variable) { - $form[] = TextInput::make('variables.'.$variable) - ->label($variable) - ->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']); - } - - return [ - Action::make('execute') - ->icon('heroicon-o-bolt') - ->modalWidth(MaxWidth::Large) - ->form($form) - ->action(function (array $data): void { - app(ExecuteScript::class)->execute($this->script, $data); - - $this->dispatch('$refresh'); - }), - ]; - } -} diff --git a/app/Web/Pages/Scripts/Index.php b/app/Web/Pages/Scripts/Index.php deleted file mode 100644 index f49929c7..00000000 --- a/app/Web/Pages/Scripts/Index.php +++ /dev/null @@ -1,78 +0,0 @@ -user()?->can('viewAny', Script::class) ?? false; - } - - public function getWidgets(): array - { - return [ - [Widgets\ScriptsList::class], - ]; - } - - protected function getHeaderActions(): array - { - /** @var User $user */ - $user = auth()->user(); - - return [ - Action::make('read-the-docs') - ->label('Read the Docs') - ->icon('heroicon-o-document-text') - ->color('gray') - ->url('https://vitodeploy.com/scripts') - ->openUrlInNewTab(), - Action::make('create') - ->label('Create a Script') - ->icon('heroicon-o-plus') - ->authorize('create', Script::class) - ->modalWidth(MaxWidth::ThreeExtraLarge) - ->form([ - TextInput::make('name') - ->rules(CreateScript::rules()['name']), - CodeEditorField::make('content') - ->rules(CreateScript::rules()['content']) - ->helperText('You can use variables like ${VARIABLE_NAME} in the script. The variables will be asked when executing the script'), - Checkbox::make('global') - ->label('Is Global (Accessible in all projects)'), - ]) - ->modalSubmitActionLabel('Create') - ->action(function (array $data) use ($user): void { - run_action($this, function () use ($data, $user): void { - app(CreateScript::class)->create($user, $data); - - $this->dispatch('$refresh'); - }); - }), - ]; - } -} diff --git a/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php b/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php deleted file mode 100644 index a707605a..00000000 --- a/app/Web/Pages/Scripts/Widgets/ScriptExecutionsList.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ - protected $listeners = ['$refresh']; - - public Script $script; - - /** - * @return Builder - */ - protected function getTableQuery(): Builder - { - return ScriptExecution::query()->where('script_id', $this->script->id); - } - - /** - * @param Builder $query - * @return Builder - */ - protected function applyDefaultSortingToTableQuery(Builder $query): Builder - { - return $query->latest('created_at'); - } - - protected function getTableColumns(): array - { - return [ - TextColumn::make('server') - ->formatStateUsing(fn (ScriptExecution $record) => $record->getServer()->name ?? 'Unknown') - ->url(function (ScriptExecution $record): ?string { - $server = $record->getServer(); - - return $server instanceof \App\Models\Server ? View::getUrl(['server' => $server]) : null; - }) - ->searchable() - ->sortable(), - TextColumn::make('created_at') - ->label('Executed At') - ->formatStateUsing(fn (ScriptExecution $record) => $record->created_at_by_timezone) - ->searchable() - ->sortable(), - TextColumn::make('status') - ->label('Status') - ->badge() - ->color(fn (ScriptExecution $record) => ScriptExecution::$statusColors[$record->status]) - ->sortable(), - ]; - } - - public function table(Table $table): Table - { - return $table - ->heading(null) - ->query($this->getTableQuery()) - ->columns($this->getTableColumns()) - ->actions([ - Action::make('logs') - ->hiddenLabel() - ->tooltip('Logs') - ->icon('heroicon-o-eye') - ->authorize(fn (ScriptExecution $record) => auth()->user()?->can('view', $record->serverLog)) - ->modalHeading('View Log') - ->modalContent(fn (ScriptExecution $record) => view('components.console-view', [ - 'slot' => $record->serverLog?->getContent(), - 'attributes' => new ComponentAttributeBag, - ])) - ->modalSubmitAction(false) - ->modalCancelActionLabel('Close'), - ]); - } -} diff --git a/app/Web/Pages/Scripts/Widgets/ScriptsList.php b/app/Web/Pages/Scripts/Widgets/ScriptsList.php deleted file mode 100644 index 9fd23bf0..00000000 --- a/app/Web/Pages/Scripts/Widgets/ScriptsList.php +++ /dev/null @@ -1,100 +0,0 @@ - - */ - protected $listeners = ['$refresh']; - - /** - * @return Builder + + {{-- Inline style to set the HTML background color based on our theme in app.css --}} + + + {{ config('app.name', 'Laravel') }} + + + + + + + + + @routes + @viteReactRefresh + @vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"]) + @inertiaHead + + + @inertia + + diff --git a/resources/views/components/app-version.blade.php b/resources/views/components/app-version.blade.php deleted file mode 100644 index 6dfc81e0..00000000 --- a/resources/views/components/app-version.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/resources/views/components/brand.blade.php b/resources/views/components/brand.blade.php deleted file mode 100644 index b8527143..00000000 --- a/resources/views/components/brand.blade.php +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/views/components/console-view.blade.php b/resources/views/components/console-view.blade.php deleted file mode 100644 index 3e3fcfbd..00000000 --- a/resources/views/components/console-view.blade.php +++ /dev/null @@ -1,5 +0,0 @@ -
merge(["class" => "font-mono relative h-[500px] w-full overflow-auto whitespace-pre-line rounded-xl border border-gray-200 bg-black p-5 text-gray-50 dark:border-gray-800"]) }} -> - {{ $slot }} -
diff --git a/resources/views/components/console.blade.php b/resources/views/components/console.blade.php deleted file mode 100644 index e02e920c..00000000 --- a/resources/views/components/console.blade.php +++ /dev/null @@ -1,166 +0,0 @@ -
id.dir", "~") }}', - command: '', - output: '', - serverName: '{{ $server->name }}', - shellPrefix: '', - clearAfterCommand: false, - runUrl: '{{ route("servers.console.run", ["server" => $server]) }}', - init() { - this.setShellPrefix() - $watch('user', async (value) => { - await this.getWorkingDir() - }) - const consoleOutput = document.getElementById('console-output') - consoleOutput.addEventListener('mouseup', (event) => { - if (window.getSelection()?.toString()) { - return - } - this.focusCommand() - }) - this.focusCommand() - - document.addEventListener('keydown', (event) => { - if (event.ctrlKey && event.key === 'l') { - event.preventDefault() - if (this.running) return - this.output = '' - } - }) - }, - async run() { - if (! this.command) return - this.running = true - let output = this.shellPrefix + ' ' + this.command + '\n' - if (this.clearAfterCommand) { - this.output = output - } else { - this.output += output - } - setTimeout(() => { - document.getElementById('console-output').scrollTop = - document.getElementById('console-output').scrollHeight - }, 100) - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - }, - body: JSON.stringify({ - user: this.user, - command: this.command, - }), - } - - this.command = '' - const response = await fetch(this.runUrl, fetchOptions) - const reader = response.body.getReader() - const decoder = new TextDecoder('utf-8') - this.setShellPrefix() - - while (true) { - if (! this.running) { - reader.cancel() - this.output += '\nStopped!' - break - } - const { value, done } = await reader.read() - if (done) break - - const textChunk = decoder.decode(value, { stream: true }) - - this.output += textChunk - - setTimeout(() => { - document.getElementById('console-output').scrollTop = - document.getElementById('console-output').scrollHeight - }, 100) - } - this.output += '\n' - await this.getWorkingDir() - this.running = false - setTimeout(() => { - document.getElementById('command').focus() - }, 100) - }, - stop() { - this.running = false - }, - setShellPrefix() { - this.shellPrefix = `${this.user}@${this.serverName}:${this.dir}$` - }, - focusCommand() { - document.getElementById('command').focus() - }, - async getWorkingDir() { - const response = await fetch( - '{{ route("servers.console.working-dir", ["server" => $server]) }}', - ) - if (response.ok) { - const data = await response.json() - this.dir = data.dir - this.setShellPrefix() - } - }, - }" -> -
-
- - - @foreach ($server->getSshUsers() as $user) - - @endforeach - - -
- - Clear - - - Stop - -
-
- -
-
-
-
-
- - -
-
-
-
diff --git a/resources/views/components/container.blade.php b/resources/views/components/container.blade.php deleted file mode 100644 index 1960eb32..00000000 --- a/resources/views/components/container.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -
- {!! $content !!} -
diff --git a/resources/views/components/dynamic-widget.blade.php b/resources/views/components/dynamic-widget.blade.php deleted file mode 100644 index 2fb7c216..00000000 --- a/resources/views/components/dynamic-widget.blade.php +++ /dev/null @@ -1 +0,0 @@ -@livewire($widget, $params ?? [], key($widget)) diff --git a/resources/views/components/form.blade.php b/resources/views/components/form.blade.php deleted file mode 100644 index 77fe8f8c..00000000 --- a/resources/views/components/form.blade.php +++ /dev/null @@ -1,6 +0,0 @@ -
-
- {{ $this->form }} -
- -
diff --git a/resources/views/components/infolist.blade.php b/resources/views/components/infolist.blade.php deleted file mode 100644 index 251638c7..00000000 --- a/resources/views/components/infolist.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -
- {{ $this->infolist }} - -
diff --git a/resources/views/components/link.blade.php b/resources/views/components/link.blade.php deleted file mode 100644 index f23de3d4..00000000 --- a/resources/views/components/link.blade.php +++ /dev/null @@ -1 +0,0 @@ -{{ $text }} diff --git a/resources/views/components/page.blade.php b/resources/views/components/page.blade.php deleted file mode 100644 index f0e1f93f..00000000 --- a/resources/views/components/page.blade.php +++ /dev/null @@ -1,11 +0,0 @@ -
getExtraAttributesBag() }}> - - @if (method_exists($this, "getSecondSubNavigation") && count($this->getSecondSubNavigation()) > 0) - - @endif - - @foreach ($this->getWidgets() as $key => $widget) - @livewire($widget[0], $widget[1] ?? [], key(class_basename($widget[0]) . "-" . $key)) - @endforeach - -
diff --git a/resources/views/components/progress-bar.blade.php b/resources/views/components/progress-bar.blade.php deleted file mode 100644 index efd92b60..00000000 --- a/resources/views/components/progress-bar.blade.php +++ /dev/null @@ -1,13 +0,0 @@ -@props(["value" => 0]) - -
-
- - {{ $value }}% - -
diff --git a/resources/views/fields/alert.blade.php b/resources/views/fields/alert.blade.php deleted file mode 100644 index f06ae346..00000000 --- a/resources/views/fields/alert.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -
-
-
- {!! $getMessage() !!} -
-
-
diff --git a/resources/views/fields/code-editor.blade.php b/resources/views/fields/code-editor.blade.php deleted file mode 100644 index 0681d22c..00000000 --- a/resources/views/fields/code-editor.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - -
-
-
merge(["class" => "mt-1 min-h-[400px] w-full border border-gray-100 dark:border-gray-700"]) }} - class="ace-vito ace_dark" - >
- -
-
-
diff --git a/resources/views/fields/provider.blade.php b/resources/views/fields/provider.blade.php deleted file mode 100644 index 85bcd80f..00000000 --- a/resources/views/fields/provider.blade.php +++ /dev/null @@ -1,19 +0,0 @@ - - {{ $getLabel() }} - -
- @foreach (config("core.server_providers") as $p) -
-
- - -
-
- @endforeach -
diff --git a/resources/views/vendor/filament-panels/components/header/simple.blade.php b/resources/views/vendor/filament-panels/components/header/simple.blade.php deleted file mode 100644 index d157bbea..00000000 --- a/resources/views/vendor/filament-panels/components/header/simple.blade.php +++ /dev/null @@ -1,27 +0,0 @@ -@props([ - "heading" => null, - "logo" => true, - "subheading" => null, -]) - -
-
- @if ($logo) - - @endif - - @if (filled($heading)) -

- {{ $heading }} -

- @endif -
- - @if (filled($subheading)) -

- {{ $subheading }} -

- @endif -
diff --git a/resources/views/widgets/select-project.blade.php b/resources/views/widgets/select-project.blade.php deleted file mode 100644 index e3fd5976..00000000 --- a/resources/views/widgets/select-project.blade.php +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - @foreach ($projects as $project) - - {{ $project->name }} - - @endforeach - - diff --git a/tests/Feature/API/ProjectsTest.php b/tests/Feature/API/ProjectsTest.php index 43e358f7..534d3339 100644 --- a/tests/Feature/API/ProjectsTest.php +++ b/tests/Feature/API/ProjectsTest.php @@ -3,10 +3,8 @@ namespace Tests\Feature\API; use App\Models\Project; -use App\Web\Pages\Settings\Projects\Settings; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; -use Livewire\Livewire; use Tests\TestCase; class ProjectsTest extends TestCase @@ -50,10 +48,7 @@ public function test_delete_project(): void $this->user->projects()->attach($project); - Livewire::test(Settings::class, [ - 'project' => $project, - ]) - ->callAction('delete') + $this->json('DELETE', '/api/projects/'.$project->id) ->assertSuccessful(); $this->assertDatabaseMissing('projects', [ diff --git a/tests/Feature/API/SourceControlsTest.php b/tests/Feature/API/SourceControlsTest.php index f269151d..f3f01ea0 100644 --- a/tests/Feature/API/SourceControlsTest.php +++ b/tests/Feature/API/SourceControlsTest.php @@ -3,11 +3,9 @@ namespace Tests\Feature\API; use App\Models\SourceControl; -use App\Web\Pages\Settings\SourceControls\Widgets\SourceControlsList; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use Laravel\Sanctum\Sanctum; -use Livewire\Livewire; use Tests\TestCase; class SourceControlsTest extends TestCase @@ -76,9 +74,14 @@ public function test_cannot_delete_provider(string $provider): void 'source_control_id' => $sourceControl->id, ]); - Livewire::test(SourceControlsList::class) - ->callTableAction('delete', $sourceControl->id) - ->assertNotified('This source control is being used by a site.'); + $this->json('DELETE', route('api.projects.source-controls.delete', [ + 'project' => $this->user->current_project_id, + 'sourceControl' => $sourceControl->id, + ])) + ->assertStatus(422) + ->assertJsonFragment([ + 'message' => 'This source control is being used by a site.', + ]); $this->assertNotSoftDeleted('source_controls', [ 'id' => $sourceControl->id, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..eaa96a31 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,122 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "baseUrl": ".", + "paths": { + "@/*": ["./resources/js/*"], + "ziggy-js": ["./vendor/tightenco/ziggy"] + }, + "jsx": "react-jsx", + }, + "include": [ + "resources/js/**/*.ts", + "resources/js/**/*.d.ts", + "resources/js/**/*.tsx", + ] +} diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 36b71150..00000000 --- a/vite.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from "vite"; -import laravel from "laravel-vite-plugin"; - -export default defineConfig({ - build: { - chunkSizeWarningLimit: 500, - }, - plugins: [ - laravel({ - input: [ - "resources/js/app.js", - "resources/css/filament/app/theme.css", - ], - refresh: true, - }), - ], -}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..290d90e8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,25 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import laravel from 'laravel-vite-plugin'; +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.tsx'], + ssr: 'resources/js/ssr.tsx', + refresh: true, + }), + react(), + tailwindcss(), + ], + esbuild: { + jsx: 'automatic', + }, + resolve: { + alias: { + 'ziggy-js': resolve(__dirname, 'vendor/tightenco/ziggy'), + }, + }, +});