diff --git a/app/Actions/Projects/CreateProject.php b/app/Actions/Projects/CreateProject.php index 839f1202..45c4137e 100644 --- a/app/Actions/Projects/CreateProject.php +++ b/app/Actions/Projects/CreateProject.php @@ -10,10 +10,13 @@ class CreateProject { public function create(User $user, array $input): Project { + if (isset($input['name'])) { + $input['name'] = strtolower($input['name']); + } + $this->validate($user, $input); $project = new Project([ - 'user_id' => $user->id, 'name' => $input['name'], ]); @@ -29,7 +32,7 @@ private function validate(User $user, array $input): void 'required', 'string', 'max:255', - 'unique:projects,name,NULL,id,user_id,'.$user->id, + 'unique:projects,name', ], ])->validate(); } diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php new file mode 100644 index 00000000..516de094 --- /dev/null +++ b/app/Actions/User/CreateUser.php @@ -0,0 +1,40 @@ +<?php + +namespace App\Actions\User; + +use App\Enums\UserRole; +use App\Models\User; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; + +class CreateUser +{ + public function create(array $input): User + { + $this->validate($input); + + /** @var User $user */ + $user = User::query()->create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'role' => $input['role'], + 'password' => bcrypt($input['password']), + 'timezone' => 'UTC', + ]); + + return $user; + } + + private function validate(array $input): void + { + Validator::make($input, [ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email', + 'password' => 'required|string|min:8', + 'role' => [ + 'required', + Rule::in([UserRole::ADMIN, UserRole::USER]), + ], + ])->validate(); + } +} diff --git a/app/Actions/User/UpdateUser.php b/app/Actions/User/UpdateUser.php new file mode 100644 index 00000000..70fc0c5f --- /dev/null +++ b/app/Actions/User/UpdateUser.php @@ -0,0 +1,48 @@ +<?php + +namespace App\Actions\User; + +use App\Enums\UserRole; +use App\Models\User; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; + +class UpdateUser +{ + public function update(User $user, array $input): void + { + $this->validate($user, $input); + + $user->name = $input['name']; + $user->email = $input['email']; + $user->timezone = $input['timezone']; + $user->role = $input['role']; + + if (isset($input['password']) && $input['password'] !== null) { + $user->password = bcrypt($input['password']); + } + + $user->save(); + } + + private function validate(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], + 'timezone' => [ + 'required', + Rule::in(timezone_identifiers_list()), + ], + 'role' => [ + 'required', + Rule::in([UserRole::ADMIN, UserRole::USER]), + function ($attribute, $value, $fail) use ($user) { + if ($user->is(auth()->user()) && $value !== $user->role) { + $fail('You cannot change your own role'); + } + }, + ], + ])->validate(); + } +} diff --git a/app/Console/Commands/CreateUserCommand.php b/app/Console/Commands/CreateUserCommand.php index 187431b0..c3bd6eea 100644 --- a/app/Console/Commands/CreateUserCommand.php +++ b/app/Console/Commands/CreateUserCommand.php @@ -7,7 +7,7 @@ class CreateUserCommand extends Command { - protected $signature = 'user:create {name} {email} {password}'; + protected $signature = 'user:create {name} {email} {password} {--role=admin}'; protected $description = 'Create a new user'; @@ -25,6 +25,7 @@ public function handle(): void 'name' => $this->argument('name'), 'email' => $this->argument('email'), 'password' => bcrypt($this->argument('password')), + 'role' => $this->option('role'), ]); $this->info('User created!'); diff --git a/app/Enums/UserRole.php b/app/Enums/UserRole.php new file mode 100644 index 00000000..57fc7ebc --- /dev/null +++ b/app/Enums/UserRole.php @@ -0,0 +1,10 @@ +<?php + +namespace App\Enums; + +final class UserRole +{ + const USER = 'user'; + + const ADMIN = 'admin'; +} diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 47ce228e..91548601 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -22,6 +22,8 @@ class ApplicationController extends Controller { public function deploy(Server $server, Site $site): HtmxResponse { + $this->authorize('manage', $server); + try { app(Deploy::class)->run($site); @@ -41,11 +43,15 @@ public function deploy(Server $server, Site $site): HtmxResponse public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse { + $this->authorize('manage', $server); + return back()->with('content', $deployment->log?->getContent()); } public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse { + $this->authorize('manage', $server); + app(UpdateDeploymentScript::class)->update($site, $request->input()); Toast::success('Deployment script updated!'); @@ -55,6 +61,8 @@ public function updateDeploymentScript(Server $server, Site $site, Request $requ public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse { + $this->authorize('manage', $server); + app(UpdateBranch::class)->update($site, $request->input()); Toast::success('Branch updated!'); @@ -64,11 +72,15 @@ public function updateBranch(Server $server, Site $site, Request $request): Redi public function getEnv(Server $server, Site $site): RedirectResponse { + $this->authorize('manage', $server); + return back()->with('env', $site->getEnv()); } public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse { + $this->authorize('manage', $server); + app(UpdateEnv::class)->update($site, $request->input()); Toast::success('Env updated!'); @@ -78,6 +90,8 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec public function enableAutoDeployment(Server $server, Site $site): HtmxResponse { + $this->authorize('manage', $server); + if (! $site->isAutoDeployment()) { try { $site->enableAutoDeployment(); @@ -101,6 +115,8 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse public function disableAutoDeployment(Server $server, Site $site): HtmxResponse { + $this->authorize('manage', $server); + if ($site->isAutoDeployment()) { try { $site->disableAutoDeployment(); diff --git a/app/Http/Controllers/ConsoleController.php b/app/Http/Controllers/ConsoleController.php index 51639f18..6905858d 100644 --- a/app/Http/Controllers/ConsoleController.php +++ b/app/Http/Controllers/ConsoleController.php @@ -11,6 +11,8 @@ class ConsoleController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('console.index', [ 'server' => $server, ]); @@ -18,6 +20,8 @@ public function index(Server $server): View public function run(Server $server, Request $request) { + $this->authorize('manage', $server); + $this->validate($request, [ 'user' => [ 'required', diff --git a/app/Http/Controllers/CronjobController.php b/app/Http/Controllers/CronjobController.php index 2c55c3c7..e958d050 100644 --- a/app/Http/Controllers/CronjobController.php +++ b/app/Http/Controllers/CronjobController.php @@ -16,6 +16,8 @@ class CronjobController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('cronjobs.index', [ 'server' => $server, 'cronjobs' => $server->cronJobs, @@ -24,6 +26,8 @@ public function index(Server $server): View public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(CreateCronJob::class)->create($server, $request->input()); Toast::success('Cronjob created successfully.'); @@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, CronJob $cronJob): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteCronJob::class)->delete($server, $cronJob); Toast::success('Cronjob deleted successfully.'); diff --git a/app/Http/Controllers/DatabaseBackupController.php b/app/Http/Controllers/DatabaseBackupController.php index 7ab6367d..770cbb28 100644 --- a/app/Http/Controllers/DatabaseBackupController.php +++ b/app/Http/Controllers/DatabaseBackupController.php @@ -18,6 +18,8 @@ class DatabaseBackupController extends Controller { public function show(Server $server, Backup $backup): View { + $this->authorize('manage', $server); + return view('databases.backups', [ 'server' => $server, 'databases' => $server->databases, @@ -28,6 +30,8 @@ public function show(Server $server, Backup $backup): View public function run(Server $server, Backup $backup): RedirectResponse { + $this->authorize('manage', $server); + app(RunBackup::class)->run($backup); Toast::success('Backup is running.'); @@ -37,6 +41,8 @@ public function run(Server $server, Backup $backup): RedirectResponse public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(CreateBackup::class)->create('database', $server, $request->input()); Toast::success('Backup created successfully.'); @@ -46,6 +52,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, Backup $backup): RedirectResponse { + $this->authorize('manage', $server); + $backup->delete(); Toast::success('Backup deleted successfully.'); @@ -55,6 +63,8 @@ public function destroy(Server $server, Backup $backup): RedirectResponse public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(RestoreBackup::class)->restore($backupFile, $request->input()); Toast::success('Backup restored successfully.'); @@ -64,6 +74,8 @@ public function restore(Server $server, Backup $backup, BackupFile $backupFile, public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse { + $this->authorize('manage', $server); + $backupFile->delete(); Toast::success('Backup file deleted successfully.'); diff --git a/app/Http/Controllers/DatabaseController.php b/app/Http/Controllers/DatabaseController.php index dafd84ff..4b5c9460 100644 --- a/app/Http/Controllers/DatabaseController.php +++ b/app/Http/Controllers/DatabaseController.php @@ -17,6 +17,8 @@ class DatabaseController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('databases.index', [ 'server' => $server, 'databases' => $server->databases, @@ -27,6 +29,8 @@ public function index(Server $server): View public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $database = app(CreateDatabase::class)->create($server, $request->input()); if ($request->input('user')) { @@ -40,6 +44,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, Database $database): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteDatabase::class)->delete($server, $database); Toast::success('Database deleted successfully.'); diff --git a/app/Http/Controllers/DatabaseUserController.php b/app/Http/Controllers/DatabaseUserController.php index 945a908a..939dffda 100644 --- a/app/Http/Controllers/DatabaseUserController.php +++ b/app/Http/Controllers/DatabaseUserController.php @@ -16,6 +16,8 @@ class DatabaseUserController extends Controller { public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $database = app(CreateDatabaseUser::class)->create($server, $request->input()); if ($request->input('user')) { @@ -29,6 +31,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteDatabaseUser::class)->delete($server, $databaseUser); Toast::success('User deleted successfully.'); @@ -38,6 +42,8 @@ public function destroy(Server $server, DatabaseUser $databaseUser): RedirectRes public function password(Server $server, DatabaseUser $databaseUser): RedirectResponse { + $this->authorize('manage', $server); + return back()->with([ 'password' => $databaseUser->password, ]); @@ -45,6 +51,8 @@ public function password(Server $server, DatabaseUser $databaseUser): RedirectRe public function link(Server $server, DatabaseUser $databaseUser, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(LinkUser::class)->link($databaseUser, $request->input()); Toast::success('Database linked successfully.'); diff --git a/app/Http/Controllers/FirewallController.php b/app/Http/Controllers/FirewallController.php index 218bb85f..d52fecc2 100644 --- a/app/Http/Controllers/FirewallController.php +++ b/app/Http/Controllers/FirewallController.php @@ -16,6 +16,8 @@ class FirewallController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('firewall.index', [ 'server' => $server, 'rules' => $server->firewallRules, @@ -24,6 +26,8 @@ public function index(Server $server): View public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(CreateRule::class)->create($server, $request->input()); Toast::success('Firewall rule created!'); @@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, FirewallRule $firewallRule): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteRule::class)->delete($server, $firewallRule); Toast::success('Firewall rule deleted!'); diff --git a/app/Http/Controllers/MetricController.php b/app/Http/Controllers/MetricController.php index 6d83190c..060f96ca 100644 --- a/app/Http/Controllers/MetricController.php +++ b/app/Http/Controllers/MetricController.php @@ -15,6 +15,8 @@ class MetricController extends Controller { public function index(Server $server, Request $request): View|RedirectResponse { + $this->authorize('manage', $server); + $this->checkIfMonitoringServiceInstalled($server); return view('metrics.index', [ @@ -26,6 +28,8 @@ public function index(Server $server, Request $request): View|RedirectResponse public function settings(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $this->checkIfMonitoringServiceInstalled($server); app(UpdateMetricSettings::class)->update($server, $request->input()); @@ -37,6 +41,8 @@ public function settings(Server $server, Request $request): HtmxResponse private function checkIfMonitoringServiceInstalled(Server $server): void { + $this->authorize('manage', $server); + if (! $server->monitoring()) { abort(404, 'Monitoring service is not installed on this server'); } diff --git a/app/Http/Controllers/PHPController.php b/app/Http/Controllers/PHPController.php index 492ed973..07bf6db0 100644 --- a/app/Http/Controllers/PHPController.php +++ b/app/Http/Controllers/PHPController.php @@ -20,6 +20,8 @@ class PHPController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('php.index', [ 'server' => $server, 'phps' => $server->services()->where('type', 'php')->get(), @@ -29,6 +31,8 @@ public function index(Server $server): View public function install(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + try { app(InstallNewPHP::class)->install($server, $request->input()); @@ -42,6 +46,8 @@ public function install(Server $server, Request $request): HtmxResponse public function installExtension(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(InstallPHPExtension::class)->install($server, $request->input()); Toast::success('PHP extension is being installed! Check the logs'); @@ -51,6 +57,8 @@ public function installExtension(Server $server, Request $request): HtmxResponse public function defaultCli(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(ChangeDefaultCli::class)->change($server, $request->input()); Toast::success('Default PHP CLI is being changed!'); @@ -60,6 +68,8 @@ public function defaultCli(Server $server, Request $request): HtmxResponse public function getIni(Server $server, Request $request): RedirectResponse { + $this->authorize('manage', $server); + $ini = app(GetPHPIni::class)->getIni($server, $request->input()); return back()->with('ini', $ini); @@ -67,6 +77,8 @@ public function getIni(Server $server, Request $request): RedirectResponse public function updateIni(Server $server, Request $request): RedirectResponse { + $this->authorize('manage', $server); + app(UpdatePHPIni::class)->update($server, $request->input()); Toast::success('PHP ini updated!'); @@ -78,6 +90,8 @@ public function updateIni(Server $server, Request $request): RedirectResponse public function uninstall(Server $server, Request $request): RedirectResponse { + $this->authorize('manage', $server); + app(UninstallPHP::class)->uninstall($server, $request->input()); Toast::success('PHP is being uninstalled!'); diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/ProfileController.php similarity index 87% rename from app/Http/Controllers/Settings/ProfileController.php rename to app/Http/Controllers/ProfileController.php index 4476c6a3..cd49a353 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -1,11 +1,10 @@ <?php -namespace App\Http\Controllers\Settings; +namespace App\Http\Controllers; use App\Actions\User\UpdateUserPassword; use App\Actions\User\UpdateUserProfileInformation; use App\Facades\Toast; -use App\Http\Controllers\Controller; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -14,7 +13,7 @@ class ProfileController extends Controller { public function index(): View { - return view('settings.profile.index'); + return view('profile.index'); } public function info(Request $request): RedirectResponse diff --git a/app/Http/Controllers/QueueController.php b/app/Http/Controllers/QueueController.php index 3009aeea..c37a64aa 100644 --- a/app/Http/Controllers/QueueController.php +++ b/app/Http/Controllers/QueueController.php @@ -19,6 +19,8 @@ class QueueController extends Controller { public function index(Server $server, Site $site): View { + $this->authorize('manage', $server); + return view('queues.index', [ 'server' => $server, 'site' => $site, @@ -28,6 +30,8 @@ public function index(Server $server, Site $site): View public function store(Server $server, Site $site, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(CreateQueue::class)->create($site, $request->input()); Toast::success('Queue is being created.'); @@ -37,6 +41,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons public function action(Server $server, Site $site, Queue $queue, string $action): HtmxResponse { + $this->authorize('manage', $server); + app(ManageQueue::class)->{$action}($queue); Toast::success('Queue is about to '.$action); @@ -46,6 +52,8 @@ public function action(Server $server, Site $site, Queue $queue, string $action) public function destroy(Server $server, Site $site, Queue $queue): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteQueue::class)->delete($queue); Toast::success('Queue is being deleted.'); @@ -55,6 +63,8 @@ public function destroy(Server $server, Site $site, Queue $queue): RedirectRespo public function logs(Server $server, Site $site, Queue $queue): RedirectResponse { + $this->authorize('manage', $server); + return back()->with('content', app(GetQueueLogs::class)->getLogs($queue)); } } diff --git a/app/Http/Controllers/SSHKeyController.php b/app/Http/Controllers/SSHKeyController.php index 4d7f0f5e..c576ebde 100644 --- a/app/Http/Controllers/SSHKeyController.php +++ b/app/Http/Controllers/SSHKeyController.php @@ -17,6 +17,8 @@ class SSHKeyController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('server-ssh-keys.index', [ 'server' => $server, 'keys' => $server->sshKeys, @@ -25,6 +27,8 @@ public function index(Server $server): View public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + /** @var \App\Models\SshKey $key */ $key = app(CreateSshKey::class)->create( $request->user(), @@ -38,6 +42,8 @@ public function store(Server $server, Request $request): HtmxResponse public function destroy(Server $server, SshKey $sshKey): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteKeyFromServer::class)->delete($server, $sshKey); Toast::success('SSH Key has been deleted.'); @@ -47,6 +53,8 @@ public function destroy(Server $server, SshKey $sshKey): RedirectResponse public function deploy(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(DeployKeyToServer::class)->deploy( $request->user(), $server, diff --git a/app/Http/Controllers/SSLController.php b/app/Http/Controllers/SSLController.php index 18d7d526..8523aada 100644 --- a/app/Http/Controllers/SSLController.php +++ b/app/Http/Controllers/SSLController.php @@ -17,6 +17,8 @@ class SSLController extends Controller { public function index(Server $server, Site $site): View { + $this->authorize('manage', $server); + return view('ssls.index', [ 'server' => $server, 'site' => $site, @@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View public function store(Server $server, Site $site, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(CreateSSL::class)->create($site, $request->input()); Toast::success('SSL certificate is being created.'); @@ -35,6 +39,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteSSL::class)->delete($ssl); Toast::success('SSL certificate has been deleted.'); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 712d8e7d..0c23e4be 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -4,6 +4,7 @@ use App\Models\Server; use App\Models\Site; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -23,10 +24,22 @@ public function search(Request $request): JsonResponse $query->where('name', 'like', '%'.$request->input('q').'%') ->orWhere('ip', 'like', '%'.$request->input('q').'%'); }) + ->whereHas('project', function (Builder $projectQuery) { + $projectQuery->whereHas('users', function (Builder $userQuery) { + $userQuery->where('user_id', auth()->user()->id); + }); + }) ->get(); $sites = Site::query() ->where('domain', 'like', '%'.$request->input('q').'%') + ->whereHas('server', function (Builder $serverQuery) { + $serverQuery->whereHas('project', function (Builder $projectQuery) { + $projectQuery->whereHas('users', function (Builder $userQuery) { + $userQuery->where('user_id', auth()->user()->id); + }); + }); + }) ->get(); $result = []; diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php index 0e6e67f1..f6bc4aa7 100644 --- a/app/Http/Controllers/ServerController.php +++ b/app/Http/Controllers/ServerController.php @@ -19,6 +19,9 @@ public function index(): View { /** @var User $user */ $user = auth()->user(); + + $this->authorize('viewAny', [Server::class, $user->currentProject]); + $servers = $user->currentProject->servers()->orderByDesc('created_at')->get(); return view('servers.index', compact('servers')); @@ -26,6 +29,11 @@ public function index(): View public function create(Request $request): View { + /** @var User $user */ + $user = auth()->user(); + + $this->authorize('create', [Server::class, $user->currentProject]); + $provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM)); $serverProviders = ServerProvider::query()->where('provider', $provider)->get(); @@ -40,8 +48,13 @@ public function create(Request $request): View */ public function store(Request $request): HtmxResponse { + /** @var User $user */ + $user = auth()->user(); + + $this->authorize('create', [Server::class, $user->currentProject]); + $server = app(CreateServer::class)->create( - $request->user(), + $user, $request->input() ); @@ -52,6 +65,8 @@ public function store(Request $request): HtmxResponse public function show(Server $server): View { + $this->authorize('view', $server); + return view('servers.show', [ 'server' => $server, ]); @@ -59,6 +74,8 @@ public function show(Server $server): View public function delete(Server $server): RedirectResponse { + $this->authorize('delete', $server); + $server->delete(); Toast::success('Server deleted successfully.'); diff --git a/app/Http/Controllers/ServerLogController.php b/app/Http/Controllers/ServerLogController.php index 933c3936..00c6764b 100644 --- a/app/Http/Controllers/ServerLogController.php +++ b/app/Http/Controllers/ServerLogController.php @@ -14,6 +14,8 @@ class ServerLogController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('server-logs.index', [ 'server' => $server, 'pageTitle' => __('Vito Logs'), @@ -22,6 +24,8 @@ public function index(Server $server): View public function show(Server $server, ServerLog $serverLog): RedirectResponse { + $this->authorize('manage', $server); + if ($server->id != $serverLog->server_id) { abort(404); } @@ -33,6 +37,8 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse public function remote(Server $server): View { + $this->authorize('manage', $server); + return view('server-logs.remote-logs', [ 'server' => $server, 'remote' => true, @@ -42,6 +48,8 @@ public function remote(Server $server): View public function store(Server $server, Request $request): \App\Helpers\HtmxResponse { + $this->authorize('manage', $server); + app(CreateServerLog::class)->create($server, $request->input()); Toast::success('Log added successfully.'); @@ -51,6 +59,8 @@ public function store(Server $server, Request $request): \App\Helpers\HtmxRespon public function destroy(Server $server, ServerLog $serverLog): RedirectResponse { + $this->authorize('manage', $server); + $serverLog->delete(); Toast::success('Remote log deleted successfully.'); diff --git a/app/Http/Controllers/ServerSettingController.php b/app/Http/Controllers/ServerSettingController.php index 15f689eb..a89ae604 100644 --- a/app/Http/Controllers/ServerSettingController.php +++ b/app/Http/Controllers/ServerSettingController.php @@ -15,11 +15,15 @@ class ServerSettingController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('server-settings.index', compact('server')); } public function checkConnection(Server $server): RedirectResponse|HtmxResponse { + $this->authorize('manage', $server); + $oldStatus = $server->status; $server = $server->checkConnection(); @@ -41,6 +45,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse public function reboot(Server $server): HtmxResponse { + $this->authorize('manage', $server); + app(RebootServer::class)->reboot($server); Toast::info('Server is rebooting.'); @@ -50,6 +56,8 @@ public function reboot(Server $server): HtmxResponse public function edit(Request $request, Server $server): RedirectResponse { + $this->authorize('manage', $server); + app(EditServer::class)->edit($server, $request->input()); Toast::success('Server updated.'); diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index df4967bf..3d9f03d1 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -16,6 +16,8 @@ class ServiceController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('services.index', [ 'server' => $server, 'services' => $server->services, @@ -24,6 +26,8 @@ public function index(Server $server): View public function start(Server $server, Service $service): RedirectResponse { + $this->authorize('manage', $server); + $service->start(); Toast::success('Service is being started!'); @@ -33,6 +37,8 @@ public function start(Server $server, Service $service): RedirectResponse public function stop(Server $server, Service $service): RedirectResponse { + $this->authorize('manage', $server); + $service->stop(); Toast::success('Service is being stopped!'); @@ -42,6 +48,8 @@ public function stop(Server $server, Service $service): RedirectResponse public function restart(Server $server, Service $service): RedirectResponse { + $this->authorize('manage', $server); + $service->restart(); Toast::success('Service is being restarted!'); @@ -51,6 +59,8 @@ public function restart(Server $server, Service $service): RedirectResponse public function enable(Server $server, Service $service): RedirectResponse { + $this->authorize('manage', $server); + $service->enable(); Toast::success('Service is being enabled!'); @@ -60,6 +70,8 @@ public function enable(Server $server, Service $service): RedirectResponse public function disable(Server $server, Service $service): RedirectResponse { + $this->authorize('manage', $server); + $service->disable(); Toast::success('Service is being disabled!'); @@ -69,6 +81,8 @@ public function disable(Server $server, Service $service): RedirectResponse public function install(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + app(Install::class)->install($server, $request->input()); Toast::success('Service is being installed!'); @@ -78,6 +92,8 @@ public function install(Server $server, Request $request): HtmxResponse public function uninstall(Server $server, Service $service): HtmxResponse { + $this->authorize('manage', $server); + app(Uninstall::class)->uninstall($service); Toast::success('Service is being uninstalled!'); diff --git a/app/Http/Controllers/Settings/NotificationChannelController.php b/app/Http/Controllers/Settings/NotificationChannelController.php index 88e12238..c007c4f4 100644 --- a/app/Http/Controllers/Settings/NotificationChannelController.php +++ b/app/Http/Controllers/Settings/NotificationChannelController.php @@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse Toast::success('Channel added successfully'); - return htmx()->redirect(route('notification-channels')); + return htmx()->redirect(route('settings.notification-channels')); } public function delete(int $id): RedirectResponse @@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse Toast::success('Channel deleted successfully'); - return redirect()->route('notification-channels'); + return redirect()->route('settings.notification-channels'); } } diff --git a/app/Http/Controllers/Settings/ProjectController.php b/app/Http/Controllers/Settings/ProjectController.php index 3d1f5c25..eb8cc904 100644 --- a/app/Http/Controllers/Settings/ProjectController.php +++ b/app/Http/Controllers/Settings/ProjectController.php @@ -20,7 +20,7 @@ class ProjectController extends Controller public function index(): View { return view('settings.projects.index', [ - 'projects' => auth()->user()->projects, + 'projects' => Project::all(), ]); } @@ -30,7 +30,7 @@ public function create(Request $request): HtmxResponse Toast::success('Project created.'); - return htmx()->redirect(route('projects')); + return htmx()->redirect(route('settings.projects')); } public function update(Request $request, Project $project): HtmxResponse @@ -42,7 +42,7 @@ public function update(Request $request, Project $project): HtmxResponse Toast::success('Project updated.'); - return htmx()->redirect(route('projects')); + return htmx()->redirect(route('settings.projects')); } public function delete(Project $project): RedirectResponse @@ -74,6 +74,8 @@ public function switch($projectId): RedirectResponse /** @var Project $project */ $project = $user->projects()->findOrFail($projectId); + $this->authorize('view', $project); + $user->current_project_id = $project->id; $user->save(); diff --git a/app/Http/Controllers/Settings/SSHKeyController.php b/app/Http/Controllers/Settings/SSHKeyController.php index 5fc14959..181b827a 100644 --- a/app/Http/Controllers/Settings/SSHKeyController.php +++ b/app/Http/Controllers/Settings/SSHKeyController.php @@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse Toast::success('SSH Key added'); - return htmx()->redirect(route('ssh-keys')); + return htmx()->redirect(route('settings.ssh-keys')); } public function delete(int $id): RedirectResponse @@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse Toast::success('SSH Key deleted'); - return redirect()->route('ssh-keys'); + return redirect()->route('settings.ssh-keys'); } } diff --git a/app/Http/Controllers/Settings/ServerProviderController.php b/app/Http/Controllers/Settings/ServerProviderController.php index bae20923..9f738a56 100644 --- a/app/Http/Controllers/Settings/ServerProviderController.php +++ b/app/Http/Controllers/Settings/ServerProviderController.php @@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse Toast::success('Server provider connected.'); - return htmx()->redirect(route('server-providers')); + return htmx()->redirect(route('settings.server-providers')); } public function delete(ServerProvider $serverProvider): RedirectResponse diff --git a/app/Http/Controllers/Settings/SourceControlController.php b/app/Http/Controllers/Settings/SourceControlController.php index f3bb097a..019b20fb 100644 --- a/app/Http/Controllers/Settings/SourceControlController.php +++ b/app/Http/Controllers/Settings/SourceControlController.php @@ -29,7 +29,7 @@ public function connect(Request $request): HtmxResponse Toast::success('Source control connected.'); - return htmx()->redirect(route('source-controls')); + return htmx()->redirect(route('settings.source-controls')); } public function delete(SourceControl $sourceControl): RedirectResponse @@ -44,6 +44,6 @@ public function delete(SourceControl $sourceControl): RedirectResponse Toast::success('Source control deleted.'); - return redirect()->route('source-controls'); + return redirect()->route('settings.source-controls'); } } diff --git a/app/Http/Controllers/Settings/StorageProviderController.php b/app/Http/Controllers/Settings/StorageProviderController.php index 939c56b9..fc5b12b9 100644 --- a/app/Http/Controllers/Settings/StorageProviderController.php +++ b/app/Http/Controllers/Settings/StorageProviderController.php @@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse Toast::success('Storage provider connected.'); - return htmx()->redirect(route('storage-providers')); + return htmx()->redirect(route('settings.storage-providers')); } public function delete(StorageProvider $storageProvider): RedirectResponse diff --git a/app/Http/Controllers/Settings/UserController.php b/app/Http/Controllers/Settings/UserController.php new file mode 100644 index 00000000..9d407733 --- /dev/null +++ b/app/Http/Controllers/Settings/UserController.php @@ -0,0 +1,78 @@ +<?php + +namespace App\Http\Controllers\Settings; + +use App\Actions\User\CreateUser; +use App\Actions\User\UpdateUser; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; +use App\Http\Controllers\Controller; +use App\Models\User; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Validation\Rule; + +class UserController extends Controller +{ + public function index(): View + { + $users = User::query()->paginate(20); + + return view('settings.users.index', compact('users')); + } + + public function store(Request $request): HtmxResponse + { + $user = app(CreateUser::class)->create($request->input()); + + return htmx()->redirect(route('settings.users.show', $user)); + } + + public function show(User $user): View + { + return view('settings.users.show', [ + 'user' => $user, + ]); + } + + public function update(User $user, Request $request): RedirectResponse + { + app(UpdateUser::class)->update($user, $request->input()); + + Toast::success('User updated successfully'); + + return back(); + } + + public function updateProjects(User $user, Request $request): HtmxResponse + { + $this->validate($request, [ + 'projects.*' => [ + 'required', + Rule::exists('projects', 'id'), + ], + ]); + + $user->projects()->sync($request->projects); + + Toast::success('Projects updated successfully'); + + return htmx()->redirect(route('settings.users.show', $user)); + } + + public function destroy(User $user): RedirectResponse + { + if ($user->is(request()->user())) { + Toast::error('You cannot delete your own account'); + + return back(); + } + + $user->delete(); + + Toast::success('User deleted successfully'); + + return redirect()->route('settings.users.index'); + } +} diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index 1ad175dc..f99862ce 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -19,6 +19,8 @@ class SiteController extends Controller { public function index(Server $server): View { + $this->authorize('manage', $server); + return view('sites.index', [ 'server' => $server, 'sites' => $server->sites()->orderByDesc('id')->get(), @@ -27,6 +29,8 @@ public function index(Server $server): View public function store(Server $server, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $site = app(CreateSite::class)->create($server, $request->input()); Toast::success('Site created'); @@ -36,6 +40,8 @@ public function store(Server $server, Request $request): HtmxResponse public function create(Server $server): View { + $this->authorize('manage', $server); + return view('sites.create', [ 'server' => $server, 'type' => old('type', request()->query('type', SiteType::LARAVEL)), @@ -45,6 +51,8 @@ public function create(Server $server): View public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse { + $this->authorize('manage', $server); + if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) { if ($request->hasHeader('HX-Request')) { return htmx()->redirect(route('servers.sites.installing', [$server, $site])); @@ -61,6 +69,8 @@ public function show(Server $server, Site $site, Request $request): View|Redirec public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse { + $this->authorize('manage', $server); + if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) { if ($request->hasHeader('HX-Request')) { return htmx()->redirect(route('servers.sites.show', [$server, $site])); @@ -77,6 +87,8 @@ public function installing(Server $server, Site $site, Request $request): View|R public function destroy(Server $server, Site $site): RedirectResponse { + $this->authorize('manage', $server); + app(DeleteSite::class)->delete($site); Toast::success('Site is being deleted'); diff --git a/app/Http/Controllers/SiteLogController.php b/app/Http/Controllers/SiteLogController.php index 79c89b00..2975a996 100644 --- a/app/Http/Controllers/SiteLogController.php +++ b/app/Http/Controllers/SiteLogController.php @@ -10,6 +10,8 @@ class SiteLogController extends Controller { public function index(Server $server, Site $site): View { + $this->authorize('manage', $server); + return view('site-logs.index', [ 'server' => $server, 'site' => $site, diff --git a/app/Http/Controllers/SiteSettingController.php b/app/Http/Controllers/SiteSettingController.php index b45170e4..a17ac4d0 100644 --- a/app/Http/Controllers/SiteSettingController.php +++ b/app/Http/Controllers/SiteSettingController.php @@ -18,6 +18,8 @@ class SiteSettingController extends Controller { public function index(Server $server, Site $site): View { + $this->authorize('manage', $server); + return view('site-settings.index', [ 'server' => $server, 'site' => $site, @@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View public function getVhost(Server $server, Site $site): RedirectResponse { + $this->authorize('manage', $server); + /** @var Webserver $handler */ $handler = $server->webserver()->handler(); @@ -34,6 +38,8 @@ public function getVhost(Server $server, Site $site): RedirectResponse public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse { + $this->authorize('manage', $server); + $this->validate($request, [ 'vhost' => 'required|string', ]); @@ -53,6 +59,8 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $this->validate($request, [ 'version' => [ 'required', @@ -73,6 +81,8 @@ public function updatePHPVersion(Server $server, Site $site, Request $request): public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse { + $this->authorize('manage', $server); + $site = app(UpdateSourceControl::class)->update($site, $request->input()); Toast::success('Source control updated successfully!'); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f0394daa..a1731197 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -68,5 +68,6 @@ class Kernel extends HttpKernel 'server-is-ready' => ServerIsReadyMiddleware::class, 'handle-ssh-errors' => HandleSSHErrors::class, 'select-current-project' => SelectCurrentProject::class, + 'is-admin' => \App\Http\Middleware\IsAdmin::class, ]; } diff --git a/app/Http/Middleware/IsAdmin.php b/app/Http/Middleware/IsAdmin.php new file mode 100644 index 00000000..c5b4697e --- /dev/null +++ b/app/Http/Middleware/IsAdmin.php @@ -0,0 +1,25 @@ +<?php + +namespace App\Http\Middleware; + +use App\Enums\UserRole; +use Closure; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; + +class IsAdmin +{ + /** + * Handle an incoming request. + * + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + */ + public function handle(Request $request, Closure $next): Response + { + if (auth()->user()->role !== UserRole::ADMIN) { + abort(403, 'You are not authorized to access this page.'); + } + + return $next($request); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php index 1985d125..d354817b 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; /** @@ -53,4 +54,9 @@ public function notificationChannels(): HasMany { return $this->hasMany(NotificationChannel::class); } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'user_project')->withTimestamps(); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 55aea29c..efe30c1a 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,7 +2,9 @@ namespace App\Models; +use App\Enums\UserRole; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -30,6 +32,7 @@ * @property int $current_project_id * @property Project $currentProject * @property Collection<Project> $projects + * @property string $role */ class User extends Authenticatable { @@ -43,6 +46,7 @@ class User extends Authenticatable 'password', 'timezone', 'current_project_id', + 'role', ]; protected $hidden = [ @@ -60,7 +64,9 @@ public static function boot(): void parent::boot(); static::created(function (User $user) { - $user->createDefaultProject(); + if (Project::count() === 0) { + $user->createDefaultProject(); + } }); } @@ -117,9 +123,9 @@ public function connectedSourceControls(): array return $connectedSourceControls; } - public function projects(): HasMany + public function projects(): BelongsToMany { - return $this->hasMany(Project::class); + return $this->belongsToMany(Project::class, 'user_project')->withTimestamps(); } public function currentProject(): HasOne @@ -138,9 +144,10 @@ public function createDefaultProject(): Project if (! $project) { $project = new Project(); - $project->user_id = $this->id; - $project->name = 'Default'; + $project->name = 'default'; $project->save(); + + $project->users()->attach($this->id); } $this->current_project_id = $project->id; @@ -148,4 +155,9 @@ public function createDefaultProject(): Project return $project; } + + public function isAdmin(): bool + { + return $this->role === UserRole::ADMIN; + } } diff --git a/app/NotificationChannels/Discord.php b/app/NotificationChannels/Discord.php index 3655adf6..ce283fd8 100644 --- a/app/NotificationChannels/Discord.php +++ b/app/NotificationChannels/Discord.php @@ -35,7 +35,7 @@ public function connect(): bool __('Congratulations! 🎉'), __("You've connected your Discord to :app", ['app' => config('app.name')])."\n". __('Manage your notification channels')."\n". - route('notification-channels') + route('settings.notification-channels') ); if (! $connect) { diff --git a/app/NotificationChannels/Slack.php b/app/NotificationChannels/Slack.php index b4a68ffb..3bc28cd3 100644 --- a/app/NotificationChannels/Slack.php +++ b/app/NotificationChannels/Slack.php @@ -35,7 +35,7 @@ public function connect(): bool __('Congratulations! 🎉'), __("You've connected your Slack to :app", ['app' => config('app.name')])."\n". __('Manage your notification channels')."\n". - route('notification-channels') + route('settings.notification-channels') ); if (! $connect) { diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 00000000..7c062da9 --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,35 @@ +<?php + +namespace App\Policies; + +use App\Enums\UserRole; +use App\Models\Project; +use App\Models\User; + +class ProjectPolicy +{ + public function viewAny(User $user): bool + { + return $user->role === UserRole::ADMIN; + } + + public function view(User $user, Project $project): bool + { + return $user->role === UserRole::ADMIN || $project->users->contains($user); + } + + public function create(User $user): bool + { + return $user->role === UserRole::ADMIN; + } + + public function update(User $user, Project $project): bool + { + return $user->role === UserRole::ADMIN; + } + + public function delete(User $user, Project $project): bool + { + return $user->role === UserRole::ADMIN; + } +} diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php new file mode 100644 index 00000000..2f9eec2d --- /dev/null +++ b/app/Policies/ServerPolicy.php @@ -0,0 +1,41 @@ +<?php + +namespace App\Policies; + +use App\Enums\UserRole; +use App\Models\Project; +use App\Models\Server; +use App\Models\User; + +class ServerPolicy +{ + public function viewAny(User $user, Project $project): bool + { + return $user->role === UserRole::ADMIN || $project->users->contains($user); + } + + public function view(User $user, Server $server): bool + { + return $user->role === UserRole::ADMIN || $server->project->users->contains($user); + } + + public function create(User $user, Project $project): bool + { + return $user->role === UserRole::ADMIN || $project->users->contains($user); + } + + public function update(User $user, Server $server): bool + { + return $user->role === UserRole::ADMIN || $server->project->users->contains($user); + } + + public function delete(User $user, Server $server): bool + { + return $user->role === UserRole::ADMIN || $server->project->users->contains($user); + } + + public function manage(User $user, Server $server): bool + { + return $user->role === UserRole::ADMIN || $server->project->users->contains($user); + } +} diff --git a/app/View/Components/ProfileLayout.php b/app/View/Components/SettingsLayout.php similarity index 66% rename from app/View/Components/ProfileLayout.php rename to app/View/Components/SettingsLayout.php index b4f6c0e1..3cc54421 100644 --- a/app/View/Components/ProfileLayout.php +++ b/app/View/Components/SettingsLayout.php @@ -5,10 +5,10 @@ use Illuminate\Contracts\View\View; use Illuminate\View\Component; -class ProfileLayout extends Component +class SettingsLayout extends Component { public function render(): View { - return view('layouts.profile'); + return view('layouts.settings'); } } diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index d99018a5..239a96a0 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -16,7 +16,6 @@ class ProjectFactory extends Factory public function definition(): array { return [ - 'user_id' => $this->faker->randomNumber(), 'name' => $this->faker->name(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), diff --git a/database/factories/ServerFactory.php b/database/factories/ServerFactory.php index 72193273..7bc05112 100755 --- a/database/factories/ServerFactory.php +++ b/database/factories/ServerFactory.php @@ -7,7 +7,6 @@ use App\Enums\ServerStatus; use App\Enums\ServerType; use App\Models\Server; -use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; class ServerFactory extends Factory @@ -16,11 +15,7 @@ class ServerFactory extends Factory public function definition(): array { - /** @var User $user */ - $user = User::factory()->create(); - return [ - 'user_id' => $user->id, 'name' => $this->faker->name(), 'ssh_user' => 'vito', 'ip' => $this->faker->ipv4(), diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index d2022098..26cb238e 100755 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Enums\UserRole; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; @@ -18,6 +19,7 @@ public function definition(): array 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), 'timezone' => 'UTC', + 'role' => UserRole::ADMIN, ]; } } diff --git a/database/migrations/2023_08_13_095440_update_storage_providers_table.php b/database/migrations/2023_08_13_095440_update_storage_providers_table.php index 200140ed..b5dc1ff5 100644 --- a/database/migrations/2023_08_13_095440_update_storage_providers_table.php +++ b/database/migrations/2023_08_13_095440_update_storage_providers_table.php @@ -9,15 +9,13 @@ public function up(): void { Schema::table('storage_providers', function (Blueprint $table) { - $table->dropColumn('token'); - $table->dropColumn('refresh_token'); - $table->dropColumn('token_expires_at'); - $table->dropColumn('label'); - $table->dropColumn('connected'); $table->unsignedBigInteger('user_id')->after('id'); $table->string('profile')->after('user_id'); $table->longText('credentials')->nullable()->after('provider'); }); + Schema::table('storage_providers', function (Blueprint $table) { + $table->dropColumn(['token', 'refresh_token', 'token_expires_at', 'label', 'connected']); + }); } public function down(): void @@ -27,9 +25,9 @@ public function down(): void $table->string('refresh_token')->nullable(); $table->string('token_expires_at')->nullable(); $table->string('label')->nullable(); - $table->dropColumn('user_id'); - $table->dropColumn('profile'); - $table->dropColumn('credentials'); + }); + Schema::table('storage_providers', function (Blueprint $table) { + $table->dropColumn(['user_id', 'profile', 'credentials']); }); } }; diff --git a/database/migrations/2024_04_24_213204_add_role_to_users_table.php b/database/migrations/2024_04_24_213204_add_role_to_users_table.php new file mode 100644 index 00000000..2fdde00c --- /dev/null +++ b/database/migrations/2024_04_24_213204_add_role_to_users_table.php @@ -0,0 +1,31 @@ +<?php + +use App\Enums\UserRole; +use App\Models\User; +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('role')->default(UserRole::USER); + }); + User::query()->update(['role' => UserRole::ADMIN]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('role'); + }); + } +}; diff --git a/database/migrations/2024_04_26_122230_create_user_project_table.php b/database/migrations/2024_04_26_122230_create_user_project_table.php new file mode 100644 index 00000000..60820d98 --- /dev/null +++ b/database/migrations/2024_04_26_122230_create_user_project_table.php @@ -0,0 +1,38 @@ +<?php + +use App\Models\Project; +use App\Models\User; +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::create('user_project', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('project_id'); + $table->timestamps(); + }); + Project::all()->each(function (Project $project) { + $project->users()->attach($project->user_id); + }); + User::all()->each(function (User $user) { + $user->current_project_id = $user->projects()->first()?->id; + $user->save(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_project'); + } +}; diff --git a/database/migrations/2024_04_26_123326_drop_user_id_from_projects_table.php b/database/migrations/2024_04_26_123326_drop_user_id_from_projects_table.php new file mode 100644 index 00000000..5c6c1730 --- /dev/null +++ b/database/migrations/2024_04_26_123326_drop_user_id_from_projects_table.php @@ -0,0 +1,28 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('projects', function (Blueprint $table) { + $table->dropColumn('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('projects', function (Blueprint $table) { + $table->bigInteger('user_id')->nullable(); + }); + } +}; diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php index dbd0f62e..84bca4c0 100644 --- a/resources/views/components/dropdown.blade.php +++ b/resources/views/components/dropdown.blade.php @@ -1,4 +1,11 @@ -@props(["open" => false, "align" => "right", "width" => "48", "contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-200 bg-white py-1 text-base dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700"]) +@props([ + "open" => false, + "align" => "right", + "width" => "48", + "contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-200 bg-white py-1 text-base dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700", + "search" => false, + "searchUrl" => "", +]) @php switch ($align) { @@ -42,6 +49,28 @@ class="{{ $width }} {{ $alignmentClasses }} absolute z-50 mt-2 rounded-md" @click="open = false" > <div class="{{ $contentClasses }} rounded-md"> + @if ($search) + <div class="p-2"> + <input + type="text" + x-ref="search" + x-model="search" + x-on:keydown.window.prevent.enter="open = false" + x-on:keydown.window.prevent.escape="open = false" + x-on:keydown.window.prevent.arrow-up=" + open = true + $refs.search.focus() + " + x-on:keydown.window.prevent.arrow-down=" + open = true + $refs.search.focus() + " + class="w-full rounded-md border border-gray-200 p-2" + placeholder="Search..." + /> + </div> + @endif + {{ $content }} </div> </div> diff --git a/resources/views/components/heroicons/o-arrow-left-circle.blade.php b/resources/views/components/heroicons/o-arrow-left-circle.blade.php new file mode 100644 index 00000000..a9da3c4f --- /dev/null +++ b/resources/views/components/heroicons/o-arrow-left-circle.blade.php @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + {{ $attributes }} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> +</svg> diff --git a/resources/views/components/heroicons/o-user-group.blade.php b/resources/views/components/heroicons/o-user-group.blade.php new file mode 100644 index 00000000..87da1551 --- /dev/null +++ b/resources/views/components/heroicons/o-user-group.blade.php @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + {{ $attributes }} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" + /> +</svg> diff --git a/resources/views/components/heroicons/o-x-mark.blade.php b/resources/views/components/heroicons/o-x-mark.blade.php new file mode 100644 index 00000000..11d6f99f --- /dev/null +++ b/resources/views/components/heroicons/o-x-mark.blade.php @@ -0,0 +1,10 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + {{ $attributes }} +> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> +</svg> diff --git a/resources/views/components/select2.blade.php b/resources/views/components/select2.blade.php new file mode 100644 index 00000000..e69de29b diff --git a/resources/views/components/status.blade.php b/resources/views/components/status.blade.php index 2ef5f143..70f363de 100644 --- a/resources/views/components/status.blade.php +++ b/resources/views/components/status.blade.php @@ -4,11 +4,11 @@ @php $class = [ - "success" => "rounded-full bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:bg-green-500 dark:bg-opacity-10", - "danger" => "rounded-full bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:bg-red-500 dark:bg-opacity-10", - "warning" => "rounded-full bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:bg-yellow-500 dark:bg-opacity-10", - "disabled" => "rounded-full bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400", - "info" => "rounded-full bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:bg-primary-500 dark:bg-opacity-10", + "success" => "rounded-md border border-green-300 bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:border-green-600 dark:bg-green-500 dark:bg-opacity-10", + "danger" => "rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:border-red-600 dark:bg-red-500 dark:bg-opacity-10", + "warning" => "rounded-md border border-yellow-300 bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:border-yellow-600 dark:bg-yellow-500 dark:bg-opacity-10", + "disabled" => "rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400", + "info" => "rounded-md border border-primary-300 bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:border-primary-600 dark:bg-primary-500 dark:bg-opacity-10", ]; @endphp diff --git a/resources/views/components/user-dropdown.blade.php b/resources/views/components/user-dropdown.blade.php index e78559f2..a50c5fd1 100644 --- a/resources/views/components/user-dropdown.blade.php +++ b/resources/views/components/user-dropdown.blade.php @@ -9,22 +9,22 @@ <x-dropdown-link :href="route('profile')"> {{ __("Profile") }} </x-dropdown-link> - <x-dropdown-link :href="route('projects')"> + <x-dropdown-link :href="route('settings.projects')"> {{ __("Projects") }} </x-dropdown-link> - <x-dropdown-link :href="route('server-providers')"> + <x-dropdown-link :href="route('settings.server-providers')"> {{ __("Server Providers") }} </x-dropdown-link> - <x-dropdown-link :href="route('source-controls')"> + <x-dropdown-link :href="route('settings.source-controls')"> {{ __("Source Controls") }} </x-dropdown-link> - <x-dropdown-link :href="route('storage-providers')"> + <x-dropdown-link :href="route('settings.storage-providers')"> {{ __("Storage Providers") }} </x-dropdown-link> - <x-dropdown-link :href="route('notification-channels')"> + <x-dropdown-link :href="route('settings.notification-channels')"> {{ __("Notification Channels") }} </x-dropdown-link> - <x-dropdown-link :href="route('ssh-keys')"> + <x-dropdown-link :href="route('settings.ssh-keys')"> {{ __("SSH Keys") }} </x-dropdown-link> <!-- Authentication --> diff --git a/resources/views/databases/partials/create-backup-modal.blade.php b/resources/views/databases/partials/create-backup-modal.blade.php index 45e148a8..6fc1782a 100644 --- a/resources/views/databases/partials/create-backup-modal.blade.php +++ b/resources/views/databases/partials/create-backup-modal.blade.php @@ -41,7 +41,7 @@ class="p-6" </option> @endforeach </x-select-input> - <x-secondary-button :href="route('storage-providers')" class="ml-2 flex-none"> + <x-secondary-button :href="route('settings.storage-providers')" class="ml-2 flex-none"> Connect </x-secondary-button> </div> diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 779fff63..9044f47e 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -3,7 +3,7 @@ class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-2 > <div class="w-full"> <div class="flex items-center justify-between"> - <div class="flex items-center justify-start"> + <div class="flex flex-none items-center justify-start"> <div class="flex items-center justify-start border-r border-gray-200 px-3 py-3 dark:border-gray-700 md:w-64" > @@ -17,7 +17,7 @@ class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gr <span class="sr-only">Open sidebar</span> <x-heroicon name="o-bars-3-center-left" class="h-6 w-6" /> </button> - <a href="/" class="ms-2 flex md:me-24"> + <a href="/" class="ms-2 flex flex-none md:me-24"> <div class="relative flex items-center justify-start text-3xl font-extrabold"> <x-application-logo class="h-9 w-9 rounded-md" /> <span class="ml-1 hidden md:block">Deploy</span> @@ -70,9 +70,11 @@ class="flex rounded-full p-1 text-sm focus:ring-2 focus:ring-gray-300 dark:focus {{ __("Profile") }} </x-dropdown-link> - <x-dropdown-link :href="route('projects')"> - {{ __("Projects") }} - </x-dropdown-link> + @if (auth()->user()->isAdmin()) + <x-dropdown-link :href="route('settings.projects')"> + {{ __("Projects") }} + </x-dropdown-link> + @endif <!-- Authentication --> <form method="POST" action="{{ route("logout") }}"> diff --git a/resources/views/layouts/partials/project-select.blade.php b/resources/views/layouts/partials/project-select.blade.php index 8e1aebb6..42298f6a 100644 --- a/resources/views/layouts/partials/project-select.blade.php +++ b/resources/views/layouts/partials/project-select.blade.php @@ -5,7 +5,14 @@ <div class="flex h-10 w-max items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 pr-7 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600" > - {{ auth()->user()->currentProject?->name ?? __("Select Project") }} + <x-heroicon name="o-inbox-stack" class="mr-2 h-4 w-4 lg:hidden" /> + <span class="hidden lg:block"> + @if (auth()->user()->currentProject &&auth()->user()->can("view", auth()->user()->currentProject)) + {{ auth()->user()->currentProject->name }} + @else + {{ __("Select Project") }} + @endif + </span> <button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2"> <x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" /> </button> @@ -14,8 +21,8 @@ class="flex h-10 w-max items-center rounded-md border border-gray-200 bg-gray-10 </x-slot> <x-slot:content> @foreach (auth()->user()->projects as $project) - <x-dropdown-link class="relative" :href="route('projects.switch', ['project' => $project])"> - <span class="block truncate">{{ ucfirst($project->name) }}</span> + <x-dropdown-link class="relative" :href="route('settings.projects.switch', ['project' => $project])"> + <span class="block truncate">{{ $project->name }}</span> @if ($project->id == auth()->user()->current_project_id) <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-primary-600"> <x-heroicon name="o-check" class="h-5 w-5" /> @@ -24,12 +31,14 @@ class="flex h-10 w-max items-center rounded-md border border-gray-200 bg-gray-10 </x-dropdown-link> @endforeach - <x-dropdown-link href="{{ route('projects') }}"> - {{ __("Projects List") }} - </x-dropdown-link> - <x-dropdown-link href="{{ route('projects', ['create' => 'open']) }}"> - {{ __("Create a Project") }} - </x-dropdown-link> + @if (auth()->user()->isAdmin()) + <x-dropdown-link href="{{ route('settings.projects') }}"> + {{ __("Projects List") }} + </x-dropdown-link> + <x-dropdown-link href="{{ route('settings.projects', ['create' => 'open']) }}"> + {{ __("Create a Project") }} + </x-dropdown-link> + @endif </x-slot> </x-dropdown> </div> diff --git a/resources/views/layouts/partials/server-select.blade.php b/resources/views/layouts/partials/server-select.blade.php index 3087f784..6ab99e4b 100644 --- a/resources/views/layouts/partials/server-select.blade.php +++ b/resources/views/layouts/partials/server-select.blade.php @@ -1,35 +1,37 @@ -<div data-tooltip="Servers" class="cursor-pointer"> - <x-dropdown width="full"> - <x-slot:trigger> - <div> - <div - class="block w-full rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500" - > - {{ isset($server) ? $server->name : "Select Server" }} +@if (auth()->user()->currentProject &&auth()->user()->can("view", auth()->user()->currentProject)) + <div data-tooltip="Servers" class="cursor-pointer"> + <x-dropdown width="full"> + <x-slot:trigger> + <div> + <div + class="block w-full rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500" + > + {{ isset($server) ? $server->name : "Select Server" }} + </div> + <button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2"> + <x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" /> + </button> </div> - <button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2"> - <x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" /> - </button> - </div> - </x-slot> - <x-slot:content> - @foreach (auth()->user()->currentProject->servers as $s) - <x-dropdown-link class="relative" :href="route('servers.show', ['server' => $s])"> - <span class="block truncate">{{ ucfirst($s->name) }}</span> - @if (isset($server) && $server->id == $s->id) - <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-primary-600"> - <x-heroicon name="o-check" class="h-5 w-5" /> - </span> - @endif - </x-dropdown-link> - @endforeach + </x-slot> + <x-slot:content> + @foreach (auth()->user()->currentProject->servers as $s) + <x-dropdown-link class="relative" :href="route('servers.show', ['server' => $s])"> + <span class="block truncate">{{ ucfirst($s->name) }}</span> + @if (isset($server) && $server->id == $s->id) + <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-primary-600"> + <x-heroicon name="o-check" class="h-5 w-5" /> + </span> + @endif + </x-dropdown-link> + @endforeach - <x-dropdown-link href="{{ route('servers') }}"> - {{ __("Servers List") }} - </x-dropdown-link> - <x-dropdown-link href="{{ route('servers.create') }}"> - {{ __("Create a Server") }} - </x-dropdown-link> - </x-slot> - </x-dropdown> -</div> + <x-dropdown-link href="{{ route('servers') }}"> + {{ __("Servers List") }} + </x-dropdown-link> + <x-dropdown-link href="{{ route('servers.create') }}"> + {{ __("Create a Server") }} + </x-dropdown-link> + </x-slot> + </x-dropdown> + </div> +@endif diff --git a/resources/views/layouts/partials/site-select.blade.php b/resources/views/layouts/partials/site-select.blade.php deleted file mode 100644 index 83a24103..00000000 --- a/resources/views/layouts/partials/site-select.blade.php +++ /dev/null @@ -1,124 +0,0 @@ -<div x-data="siteCombobox()"> - <div class="relative"> - <div - @click="open = !open" - class="text-md flex h-10 w-full cursor-pointer items-center rounded-md bg-gray-200 px-4 py-3 pr-10 leading-5 focus:ring-1 focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-100" - x-text="selected.domain ?? 'Select Site'" - ></div> - <button type="button" @click="open = !open" class="absolute inset-y-0 right-0 flex items-center pr-2"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - class="h-5 w-5 text-gray-400" - > - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - ></path> - </svg> - </button> - <div - x-show="open" - @click.away="open = false" - class="absolute mt-1 w-full overflow-auto rounded-md bg-white pb-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-700 sm:text-sm" - > - <div class="relative p-2"> - <input - x-model="query" - @input="filterSitesAndOpen" - placeholder="Filter" - class="dark:focus:ring-800 w-full rounded-md bg-gray-200 py-2 pl-3 pr-10 text-sm leading-5 focus:ring-1 focus:ring-gray-400 dark:bg-gray-800 dark:text-gray-100" - /> - </div> - <div class="relative max-h-[350px] overflow-y-auto"> - <template x-for="(site, index) in filteredSites" :key="index"> - <div - @click="selectSite(site); open = false" - :class="site.id === selected.id ? 'cursor-default bg-primary-600 text-white' : 'cursor-pointer'" - class="relative select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white" - > - <span class="block truncate" x-text="site.domain"></span> - <template x-if="site.id === selected.id"> - <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-white"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - class="h-5 w-5" - > - <path - fill-rule="evenodd" - d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" - clip-rule="evenodd" - ></path> - </svg> - </span> - </template> - </div> - </template> - </div> - <div - x-show="filteredSites.length === 0" - class="relative block cursor-default select-none truncate px-4 py-2 text-gray-700 dark:text-white" - > - No sites found! - </div> - <div class="py-1"> - <hr class="border-gray-300 dark:border-gray-600" /> - </div> - <div> - <a - href="{{ route("servers.sites", ["server" => $server]) }}" - class="@if(request()->routeIs('sites')) cursor-default bg-primary-600 text-white @else cursor-pointer @endif relative block select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white" - > - <span class="block truncate">Sites List</span> - </a> - </div> - <div> - <a - href="{{ route("servers.sites.create", ["server" => $server]) }}" - class="@if(request()->routeIs('sites.create')) cursor-default bg-primary-600 text-white @else cursor-pointer @endif relative block select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white" - > - <span class="block truncate">Create a Site</span> - </a> - </div> - </div> - </div> -</div> - -<script> - function siteCombobox() { - const sites = @json(\App\Models\Site::query()->where('server_id', $server->id)->select('id', 'domain')->get()); - return { - open: false, - query: '', - sites: sites, - selected: @if(isset($site)) @json($site->only('id', 'domain')) @else {} @endif, - filteredSites: sites, - selectSite(site) { - if (this.selected.id !== site.id) { - this.selected = site; - window.location.href = '{{ url('/servers') }}/' + '{{ $server->id }}/sites/' + site.id - } - }, - filterSitesAndOpen() { - if (this.query === '') { - this.filteredSites = this.sites; - this.open = false; - } else { - this.filteredSites = this.sites.filter((site) => - site.domain - .toLowerCase() - .replace(/\s+/g, '') - .includes(this.query.toLowerCase().replace(/\s+/g, '')) - ); - this.open = true; - } - }, - }; - } -</script> diff --git a/resources/views/layouts/profile.blade.php b/resources/views/layouts/settings.blade.php similarity index 100% rename from resources/views/layouts/profile.blade.php rename to resources/views/layouts/settings.blade.php diff --git a/resources/views/layouts/sidebar.blade.php b/resources/views/layouts/sidebar.blade.php index 398c27d0..56f4c508 100644 --- a/resources/views/layouts/sidebar.blade.php +++ b/resources/views/layouts/sidebar.blade.php @@ -143,7 +143,7 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g > <x-heroicon name="o-wrench-screwdriver" class="h-6 w-6" /> <span class="ml-2"> - {{ __("Settings") }} + {{ __("Server Settings") }} </span> </x-sidebar-link> </li> @@ -168,45 +168,72 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g <span class="ml-2">Profile</span> </x-sidebar-link> </li> - <li> - <x-sidebar-link :href="route('projects')" :active="request()->routeIs('projects')"> - <x-heroicon name="o-inbox-stack" class="h-6 w-6" /> - <span class="ml-2">Projects</span> - </x-sidebar-link> - </li> - <li> - <x-sidebar-link :href="route('server-providers')" :active="request()->routeIs('server-providers')"> - <x-heroicon name="o-server-stack" class="h-6 w-6" /> - <span class="ml-2">Server Providers</span> - </x-sidebar-link> - </li> - <li> - <x-sidebar-link :href="route('source-controls')" :active="request()->routeIs('source-controls')"> - <x-heroicon name="o-code-bracket" class="h-6 w-6" /> - <span class="ml-2">Source Controls</span> - </x-sidebar-link> - </li> - <li> - <x-sidebar-link :href="route('storage-providers')" :active="request()->routeIs('storage-providers')"> - <x-heroicon name="o-circle-stack" class="h-6 w-6" /> - <span class="ml-2">Storage Providers</span> - </x-sidebar-link> - </li> - <li> - <x-sidebar-link - :href="route('notification-channels')" - :active="request()->routeIs('notification-channels')" - > - <x-heroicon name="o-bell" class="h-6 w-6" /> - <span class="ml-2">Notification Channels</span> - </x-sidebar-link> - </li> - <li> - <x-sidebar-link :href="route('ssh-keys')" :active="request()->routeIs('ssh-keys')"> - <x-heroicon name="o-key" class="h-6 w-6" /> - <span class="ml-2">SSH Keys</span> - </x-sidebar-link> - </li> + + @if (auth()->user()->isAdmin()) + <li> + <x-sidebar-link + :href="route('settings.users.index')" + :active="request()->routeIs('settings.users*')" + > + <x-heroicon name="o-user-group" class="h-6 w-6" /> + <span class="ml-2">Users</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.projects')" + :active="request()->routeIs('settings.projects')" + > + <x-heroicon name="o-inbox-stack" class="h-6 w-6" /> + <span class="ml-2">Projects</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.server-providers')" + :active="request()->routeIs('settings.server-providers')" + > + <x-heroicon name="o-server-stack" class="h-6 w-6" /> + <span class="ml-2">Server Providers</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.source-controls')" + :active="request()->routeIs('settings.source-controls')" + > + <x-heroicon name="o-code-bracket" class="h-6 w-6" /> + <span class="ml-2">Source Controls</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.storage-providers')" + :active="request()->routeIs('settings.storage-providers')" + > + <x-heroicon name="o-circle-stack" class="h-6 w-6" /> + <span class="ml-2">Storage Providers</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.notification-channels')" + :active="request()->routeIs('settings.notification-channels')" + > + <x-heroicon name="o-bell" class="h-6 w-6" /> + <span class="ml-2">Notification Channels</span> + </x-sidebar-link> + </li> + <li> + <x-sidebar-link + :href="route('settings.ssh-keys')" + :active="request()->routeIs('settings.ssh-keys')" + > + <x-heroicon name="o-key" class="h-6 w-6" /> + <span class="ml-2">SSH Keys</span> + </x-sidebar-link> + </li> + @endif </ul> </div> </aside> diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php new file mode 100644 index 00000000..ae0e0f24 --- /dev/null +++ b/resources/views/profile/index.blade.php @@ -0,0 +1,9 @@ +<x-settings-layout> + <x-slot name="pageTitle">{{ __("Profile") }}</x-slot> + + @include("profile.partials.update-profile-information") + + @include("profile.partials.update-password") + + @include("profile.partials.two-factor-authentication") +</x-settings-layout> diff --git a/resources/views/profile/partials/two-factor-authentication.blade.php b/resources/views/profile/partials/two-factor-authentication.blade.php new file mode 100644 index 00000000..1d44665b --- /dev/null +++ b/resources/views/profile/partials/two-factor-authentication.blade.php @@ -0,0 +1,78 @@ +<x-card> + <x-slot name="title"> + {{ __("Two Factor Authentication") }} + </x-slot> + + <x-slot name="description"> + {{ __("Here you can activate 2FA to secure your account") }} + </x-slot> + + <div id="two-factor"> + @if (! auth()->user()->two_factor_secret) + {{-- Enable 2FA --}} + <form + hx-post="{{ route("two-factor.enable") }}" + hx-target="#two-factor" + hx-select="#two-factor" + hx-swap="outerHTML" + > + @csrf + + <x-primary-button type="submit"> + {{ __("Enable Two-Factor") }} + </x-primary-button> + </form> + @else + {{-- Disable 2FA --}} + <form + hx-post="{{ route("two-factor.disable") }}" + hx-target="#two-factor" + hx-select="#two-factor" + hx-swap="outerHTML" + > + @csrf + @method("DELETE") + + <x-danger-button type="submit"> + {{ __("Disable Two-Factor") }} + </x-danger-button> + </form> + + @if (session("status") == "two-factor-authentication-enabled") + <div class="mt-5"> + {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }} + </div> + + <div class="mt-5"> + {!! auth()->user()->twoFactorQrCodeSvg() !!} + </div> + @endif + + {{-- Show 2FA Recovery Codes --}} + <div class="mt-5"> + {{ __("Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.") }} + </div> + + <div class="mt-5 rounded-md border border-gray-100 p-2 dark:border-gray-700"> + @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code) + <div class="mt-2">{{ $code }}</div> + @endforeach + </div> + + {{-- Regenerate 2FA Recovery Codes --}} + <form + class="mt-5" + hx-post="{{ route("two-factor.recovery-codes") }}" + hx-target="#two-factor" + hx-select="#two-factor" + hx-swap="outerHTML" + > + @csrf + + <x-primary-button type="submit"> + {{ __("Regenerate Recovery Codes") }} + </x-primary-button> + </form> + @endif + </div> +</x-card> diff --git a/resources/views/settings/profile/partials/update-password.blade.php b/resources/views/profile/partials/update-password.blade.php similarity index 100% rename from resources/views/settings/profile/partials/update-password.blade.php rename to resources/views/profile/partials/update-password.blade.php diff --git a/resources/views/settings/profile/partials/update-profile-information.blade.php b/resources/views/profile/partials/update-profile-information.blade.php similarity index 100% rename from resources/views/settings/profile/partials/update-profile-information.blade.php rename to resources/views/profile/partials/update-profile-information.blade.php diff --git a/resources/views/servers/partials/create-server.blade.php b/resources/views/servers/partials/create-server.blade.php index c32b8379..5921bf1e 100644 --- a/resources/views/servers/partials/create-server.blade.php +++ b/resources/views/servers/partials/create-server.blade.php @@ -70,7 +70,7 @@ class="mt-6 space-y-6" @endforeach </x-select-input> <x-secondary-button - :href="route('server-providers', ['provider' => $provider])" + :href="route('settings.server-providers', ['provider' => $provider])" class="ml-2 flex-none" > {{ __("Connect") }} diff --git a/resources/views/settings/notification-channels/index.blade.php b/resources/views/settings/notification-channels/index.blade.php index 3b27ab88..e09d74bc 100644 --- a/resources/views/settings/notification-channels/index.blade.php +++ b/resources/views/settings/notification-channels/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("Notification Channels") }}</x-slot> @include("settings.notification-channels.partials.channels-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/notification-channels/partials/add-channel.blade.php b/resources/views/settings/notification-channels/partials/add-channel.blade.php index a137befd..ab413c3c 100644 --- a/resources/views/settings/notification-channels/partials/add-channel.blade.php +++ b/resources/views/settings/notification-channels/partials/add-channel.blade.php @@ -10,7 +10,7 @@ <form id="add-channel-form" - hx-post="{{ route("notification-channels.add") }}" + hx-post="{{ route("settings.notification-channels.add") }}" hx-swap="outerHTML" hx-select="#add-channel-form" hx-ext="disable-element" diff --git a/resources/views/settings/notification-channels/partials/channels-list.blade.php b/resources/views/settings/notification-channels/partials/channels-list.blade.php index 74d995a8..db1b363a 100644 --- a/resources/views/settings/notification-channels/partials/channels-list.blade.php +++ b/resources/views/settings/notification-channels/partials/channels-list.blade.php @@ -24,7 +24,7 @@ <div class="flex items-center"> <div class="inline"> <x-icon-button - x-on:click="deleteAction = '{{ route('notification-channels.delete', $channel->id) }}'; $dispatch('open-modal', 'delete-channel')" + x-on:click="deleteAction = '{{ route('settings.notification-channels.delete', $channel->id) }}'; $dispatch('open-modal', 'delete-channel')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/profile/index.blade.php b/resources/views/settings/profile/index.blade.php deleted file mode 100644 index 141d25ff..00000000 --- a/resources/views/settings/profile/index.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -<x-profile-layout> - <x-slot name="pageTitle">{{ __("Profile") }}</x-slot> - - @include("settings.profile.partials.update-profile-information") - - @include("settings.profile.partials.update-password") - - @include("settings.profile.partials.two-factor-authentication") -</x-profile-layout> diff --git a/resources/views/settings/profile/partials/two-factor-authentication.blade.php b/resources/views/settings/profile/partials/two-factor-authentication.blade.php deleted file mode 100644 index 6ad8d890..00000000 --- a/resources/views/settings/profile/partials/two-factor-authentication.blade.php +++ /dev/null @@ -1,60 +0,0 @@ -<x-card> - <x-slot name="title"> - {{ __("Two Factor Authentication") }} - </x-slot> - - <x-slot name="description"> - {{ __("Here you can activate 2FA to secure your account") }} - </x-slot> - - @if (! auth()->user()->two_factor_secret) - {{-- Enable 2FA --}} - <form method="POST" action="{{ route("two-factor.enable") }}"> - @csrf - - <x-primary-button type="submit"> - {{ __("Enable Two-Factor") }} - </x-primary-button> - </form> - @else - {{-- Disable 2FA --}} - <form method="POST" action="{{ route("two-factor.disable") }}"> - @csrf - @method("DELETE") - - <x-danger-button type="submit"> - {{ __("Disable Two-Factor") }} - </x-danger-button> - </form> - - @if (session("status") == "two-factor-authentication-enabled") - <div class="mt-5"> - {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }} - </div> - - <div class="mt-5"> - {!! auth()->user()->twoFactorQrCodeSvg() !!} - </div> - @endif - - {{-- Show 2FA Recovery Codes --}} - <div class="mt-5"> - {{ __("Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.") }} - </div> - - <div class="mt-5 rounded-md border border-gray-100 p-2 dark:border-gray-700"> - @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code) - <div class="mt-2">{{ $code }}</div> - @endforeach - </div> - - {{-- Regenerate 2FA Recovery Codes --}} - <form class="mt-5" method="POST" action="{{ route("two-factor.recovery-codes") }}"> - @csrf - - <x-primary-button type="submit"> - {{ __("Regenerate Recovery Codes") }} - </x-primary-button> - </form> - @endif -</x-card> diff --git a/resources/views/settings/projects/index.blade.php b/resources/views/settings/projects/index.blade.php index 694dc05d..bdc4c95a 100644 --- a/resources/views/settings/projects/index.blade.php +++ b/resources/views/settings/projects/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("Projects") }}</x-slot> @include("settings.projects.partials.projects-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/projects/partials/create-project.blade.php b/resources/views/settings/projects/partials/create-project.blade.php index 465bc446..1d0b11f4 100644 --- a/resources/views/settings/projects/partials/create-project.blade.php +++ b/resources/views/settings/projects/partials/create-project.blade.php @@ -6,7 +6,7 @@ <x-modal name="create-project" :show="request()->has('create')"> <form id="create-project-form" - hx-post="{{ route("projects.create") }}" + hx-post="{{ route("settings.projects.create") }}" hx-swap="outerHTML" hx-select="#create-project-form" hx-ext="disable-element" diff --git a/resources/views/settings/projects/partials/edit-project.blade.php b/resources/views/settings/projects/partials/edit-project.blade.php index 8e573359..051e24d7 100644 --- a/resources/views/settings/projects/partials/edit-project.blade.php +++ b/resources/views/settings/projects/partials/edit-project.blade.php @@ -6,7 +6,7 @@ <x-modal name="edit-project-{{ $project->id }}"> <form id="edit-project-form-{{ $project->id }}" - hx-post="{{ route("projects.update", $project) }}" + hx-post="{{ route("settings.projects.update", $project) }}" hx-swap="outerHTML" hx-select="#edit-project-form-{{ $project->id }}" hx-ext="disable-element" diff --git a/resources/views/settings/projects/partials/projects-list.blade.php b/resources/views/settings/projects/partials/projects-list.blade.php index 4b3646e6..31b36d6a 100644 --- a/resources/views/settings/projects/partials/projects-list.blade.php +++ b/resources/views/settings/projects/partials/projects-list.blade.php @@ -27,7 +27,7 @@ <div class="flex items-center"> @include("settings.projects.partials.edit-project", ["project" => $project]) <x-icon-button - x-on:click="deleteAction = '{{ route('projects.delete', $project) }}'; $dispatch('open-modal', 'delete-project')" + x-on:click="deleteAction = '{{ route('settings.projects.delete', $project) }}'; $dispatch('open-modal', 'delete-project')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/server-providers/index.blade.php b/resources/views/settings/server-providers/index.blade.php index 3050e1e5..6c0e6457 100644 --- a/resources/views/settings/server-providers/index.blade.php +++ b/resources/views/settings/server-providers/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot> @include("settings.server-providers.partials.providers-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/server-providers/partials/connect-provider.blade.php b/resources/views/settings/server-providers/partials/connect-provider.blade.php index f52f3abf..012d6168 100644 --- a/resources/views/settings/server-providers/partials/connect-provider.blade.php +++ b/resources/views/settings/server-providers/partials/connect-provider.blade.php @@ -10,7 +10,7 @@ <form id="connect-provider-form" - hx-post="{{ route("server-providers.connect") }}" + hx-post="{{ route("settings.server-providers.connect") }}" hx-swap="outerHTML" hx-select="#connect-provider-form" hx-ext="disable-element" diff --git a/resources/views/settings/server-providers/partials/providers-list.blade.php b/resources/views/settings/server-providers/partials/providers-list.blade.php index 09ca8478..d02d3de9 100644 --- a/resources/views/settings/server-providers/partials/providers-list.blade.php +++ b/resources/views/settings/server-providers/partials/providers-list.blade.php @@ -26,7 +26,7 @@ class="h-10 w-10" <div class="flex items-center"> <div class="inline"> <x-icon-button - x-on:click="deleteAction = '{{ route('server-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')" + x-on:click="deleteAction = '{{ route('settings.server-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/source-controls/index.blade.php b/resources/views/settings/source-controls/index.blade.php index 609a3872..c4c4d688 100644 --- a/resources/views/settings/source-controls/index.blade.php +++ b/resources/views/settings/source-controls/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("Source Controls") }}</x-slot> @include("settings.source-controls.partials.source-controls-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/source-controls/partials/connect.blade.php b/resources/views/settings/source-controls/partials/connect.blade.php index b1280e59..edbea6f9 100644 --- a/resources/views/settings/source-controls/partials/connect.blade.php +++ b/resources/views/settings/source-controls/partials/connect.blade.php @@ -10,7 +10,7 @@ <form id="connect-source-control-form" - hx-post="{{ route("source-controls.connect") }}" + hx-post="{{ route("settings.source-controls.connect") }}" hx-swap="outerHTML" hx-select="#connect-source-control-form" hx-ext="disable-element" diff --git a/resources/views/settings/source-controls/partials/source-controls-list.blade.php b/resources/views/settings/source-controls/partials/source-controls-list.blade.php index 8453102d..2c2a9e9f 100644 --- a/resources/views/settings/source-controls/partials/source-controls-list.blade.php +++ b/resources/views/settings/source-controls/partials/source-controls-list.blade.php @@ -22,7 +22,7 @@ <div class="flex items-center"> <div class="inline"> <x-icon-button - x-on:click="deleteAction = '{{ route('source-controls.delete', $sourceControl->id) }}'; $dispatch('open-modal', 'delete-source-control')" + x-on:click="deleteAction = '{{ route('settings.source-controls.delete', $sourceControl->id) }}'; $dispatch('open-modal', 'delete-source-control')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/ssh-keys/index.blade.php b/resources/views/settings/ssh-keys/index.blade.php index 4add01ed..e21a1a90 100644 --- a/resources/views/settings/ssh-keys/index.blade.php +++ b/resources/views/settings/ssh-keys/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("SSH Keys") }}</x-slot> @include("settings.ssh-keys.partials.keys-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/ssh-keys/partials/add-key.blade.php b/resources/views/settings/ssh-keys/partials/add-key.blade.php index 04050897..54a4d76c 100644 --- a/resources/views/settings/ssh-keys/partials/add-key.blade.php +++ b/resources/views/settings/ssh-keys/partials/add-key.blade.php @@ -6,7 +6,7 @@ <x-modal name="add-key"> <form id="add-ssh-key-form" - hx-post="{{ route("ssh-keys.add") }}" + hx-post="{{ route("settings.ssh-keys.add") }}" hx-swap="outerHTML" hx-select="#add-ssh-key-form" hx-ext="disable-element" diff --git a/resources/views/settings/ssh-keys/partials/keys-list.blade.php b/resources/views/settings/ssh-keys/partials/keys-list.blade.php index c162c69f..fb51041b 100644 --- a/resources/views/settings/ssh-keys/partials/keys-list.blade.php +++ b/resources/views/settings/ssh-keys/partials/keys-list.blade.php @@ -21,7 +21,7 @@ <div class="flex items-center"> <div class="inline"> <x-icon-button - x-on:click="deleteAction = '{{ route('ssh-keys.delete', $key->id) }}'; $dispatch('open-modal', 'delete-ssh-key')" + x-on:click="deleteAction = '{{ route('settings.ssh-keys.delete', $key->id) }}'; $dispatch('open-modal', 'delete-ssh-key')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/storage-providers/index.blade.php b/resources/views/settings/storage-providers/index.blade.php index 1c4024fb..1be9a73c 100644 --- a/resources/views/settings/storage-providers/index.blade.php +++ b/resources/views/settings/storage-providers/index.blade.php @@ -1,5 +1,5 @@ -<x-profile-layout> +<x-settings-layout> <x-slot name="pageTitle">{{ __("Storage Providers") }}</x-slot> @include("settings.storage-providers.partials.providers-list") -</x-profile-layout> +</x-settings-layout> diff --git a/resources/views/settings/storage-providers/partials/connect-provider.blade.php b/resources/views/settings/storage-providers/partials/connect-provider.blade.php index 81241c10..346c0cb0 100644 --- a/resources/views/settings/storage-providers/partials/connect-provider.blade.php +++ b/resources/views/settings/storage-providers/partials/connect-provider.blade.php @@ -10,7 +10,7 @@ <form id="connect-storage-provider-form" - hx-post="{{ route("storage-providers.connect") }}" + hx-post="{{ route("settings.storage-providers.connect") }}" hx-swap="outerHTML" hx-select="#connect-storage-provider-form" hx-ext="disable-element" diff --git a/resources/views/settings/storage-providers/partials/providers-list.blade.php b/resources/views/settings/storage-providers/partials/providers-list.blade.php index bb83a17c..acb6d462 100644 --- a/resources/views/settings/storage-providers/partials/providers-list.blade.php +++ b/resources/views/settings/storage-providers/partials/providers-list.blade.php @@ -43,7 +43,7 @@ class="h-10 w-10" <div class="flex items-center"> <div class="inline"> <x-icon-button - x-on:click="deleteAction = '{{ route('storage-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')" + x-on:click="deleteAction = '{{ route('settings.storage-providers.delete', $provider->id) }}'; $dispatch('open-modal', 'delete-provider')" > <x-heroicon name="o-trash" class="h-5 w-5" /> </x-icon-button> diff --git a/resources/views/settings/users/index.blade.php b/resources/views/settings/users/index.blade.php new file mode 100644 index 00000000..7593c275 --- /dev/null +++ b/resources/views/settings/users/index.blade.php @@ -0,0 +1,61 @@ +<x-settings-layout> + <x-slot name="pageTitle">Users</x-slot> + + <x-container> + <x-card-header> + <x-slot name="title">Users</x-slot> + <x-slot name="description">Here you can manage users</x-slot> + <x-slot name="aside"> + @include("settings.users.partials.create-user") + </x-slot> + </x-card-header> + <div class="space-y-3" x-data="{ deleteAction: '' }"> + <x-table> + <x-thead> + <x-tr> + <x-th>ID</x-th> + <x-th>Name</x-th> + <x-th>Email</x-th> + <x-th>Role</x-th> + <x-th></x-th> + </x-tr> + </x-thead> + <x-tbody> + @foreach ($users as $user) + <x-tr> + <x-td>{{ $user->id }}</x-td> + <x-td>{{ $user->name }}</x-td> + <x-td>{{ $user->email }}</x-td> + <x-td> + <div class="inline-flex"> + @if ($user->role === \App\Enums\UserRole::ADMIN) + <x-status status="success">ADMIN</x-status> + @else + <x-status status="info">USER</x-status> + @endif + </div> + </x-td> + <x-td class="text-right"> + <x-icon-button + x-on:click="deleteAction = '{{ route('settings.users.delete', ['user' => $user]) }}'; $dispatch('open-modal', 'delete-user')" + > + <x-heroicon name="o-trash" class="h-5 w-5" /> + </x-icon-button> + <x-icon-button :href="route('settings.users.show', ['user' => $user])"> + <x-heroicon name="o-cog-6-tooth" class="h-5 w-5" /> + </x-icon-button> + </x-td> + </x-tr> + @endforeach + </x-tbody> + </x-table> + <x-confirmation-modal + name="delete-user" + :title="__('Confirm')" + :description="__('Are you sure that you want to delete this user?')" + method="delete" + x-bind:action="deleteAction" + /> + </div> + </x-container> +</x-settings-layout> diff --git a/resources/views/settings/users/partials/create-user.blade.php b/resources/views/settings/users/partials/create-user.blade.php new file mode 100644 index 00000000..1a9e15c4 --- /dev/null +++ b/resources/views/settings/users/partials/create-user.blade.php @@ -0,0 +1,69 @@ +<div> + <x-primary-button x-data="" x-on:click.prevent="$dispatch('open-modal', 'create-user')">New User</x-primary-button> + + <x-modal name="create-user"> + <form + id="create-user-form" + hx-post="{{ route("settings.users.store") }}" + hx-swap="outerHTML" + hx-select="#create-user-form" + hx-ext="disable-element" + hx-disable-element="#btn-create-user" + class="p-6" + > + @csrf + <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Create New User</h2> + + <div class="mt-6"> + <x-input-label for="name" value="Name" /> + <x-text-input value="{{ old('name') }}" id="name" name="name" type="text" class="mt-1 w-full" /> + @error("name") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div class="mt-6"> + <x-input-label for="email" value="Email" /> + <x-text-input value="{{ old('email') }}" id="email" name="email" type="text" class="mt-1 w-full" /> + @error("email") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div class="mt-6"> + <x-input-label for="password" value="Password" /> + <x-text-input id="password" name="password" type="password" class="mt-1 w-full" /> + @error("password") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div class="mt-6"> + <x-input-label for="role" value="Role" /> + <x-select-input id="role" name="role" class="mt-1 w-full"> + <option + value="{{ \App\Enums\UserRole::USER }}" + @if(old('role') === \App\Enums\UserRole::USER) selected @endif + > + User + </option> + <option + value="{{ \App\Enums\UserRole::ADMIN }}" + @if(old('role') === \App\Enums\UserRole::ADMIN) selected @endif + > + Admin + </option> + </x-select-input> + @error("role") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div class="mt-6 flex justify-end"> + <x-secondary-button type="button" x-on:click="$dispatch('close')">Cancel</x-secondary-button> + + <x-primary-button id="btn-create-project" class="ml-3">Create</x-primary-button> + </div> + </form> + </x-modal> +</div> diff --git a/resources/views/settings/users/partials/update-projects.blade.php b/resources/views/settings/users/partials/update-projects.blade.php new file mode 100644 index 00000000..e3aa0f04 --- /dev/null +++ b/resources/views/settings/users/partials/update-projects.blade.php @@ -0,0 +1,120 @@ +<x-card> + <x-slot name="title">Projects</x-slot> + + <x-slot name="description">Manage the projects that the user is in</x-slot> + + <x-slot name="aside"> + <x-secondary-button :href="route('settings.users.index')">Back to Users</x-secondary-button> + </x-slot> + + <form + id="update-projects" + hx-post="{{ route("settings.users.update-projects", ["user" => $user]) }}" + hx-swap="outerHTML" + hx-select="#update-projects" + hx-trigger="submit" + hx-ext="disable-element" + hx-disable-element="#btn-save-projects" + class="mt-6" + > + @csrf + + <script> + let projects = @json($user->projects); + </script> + + <div + class="space-y-6" + x-data="{ + q: '', + projects: projects, + search() { + htmx.ajax('GET', '{{ request()->getUri() }}?q=' + this.q, { + target: '#projects-list', + swap: 'outerHTML', + select: '#projects-list', + }).then(() => { + document.getElementById('q').focus() + }) + }, + addProject(project) { + if (this.projects.find((p) => p.id === project.id)) { + return + } + + this.projects.push(project) + this.q = '' + }, + removeProject(id) { + this.projects = this.projects.filter((project) => project.id !== id) + }, + }" + > + <div> + <x-input-label value="Projects" /> + + <div class="mt-1"> + <template x-for="project in projects"> + <div class="mr-1 inline-flex"> + <x-status status="info" class="flex items-center"> + <span x-text="project.name"></span> + <x-heroicon + name="o-x-mark" + class="ml-1 h-4 w-4 cursor-pointer" + x-on:click="removeProject(project.id)" + /> + <input type="hidden" name="projects[]" x-bind:value="project.id" /> + </x-status> + </div> + </template> + </div> + </div> + + <div> + <x-input-label value="Add new Project" /> + + @php + $projects = \App\Models\Project::query() + ->where(function ($query) { + if (request()->has("q")) { + $query->where("name", "like", "%" . request("q") . "%"); + } + }) + ->take(5) + ->get(); + @endphp + + <x-dropdown width="full"> + <x-slot name="trigger"> + <x-text-input + id="q" + name="q" + x-model="q" + type="text" + class="mt-1 w-full" + placeholder="Search for projects..." + autocomplete="off" + x-on:input.debounce.500ms="search" + /> + </x-slot> + <x-slot name="content"> + <div id="projects-list"> + @foreach ($projects as $project) + <x-dropdown-link + class="cursor-pointer" + x-on:click="addProject({ id: {{ $project->id }}, name: '{{ $project->name }}' })" + > + {{ $project->name }} + </x-dropdown-link> + @endforeach + </div> + </x-slot> + </x-dropdown> + </div> + </div> + </form> + + <x-slot name="actions"> + <x-primary-button id="btn-save-projects" form="update-projects">Save</x-primary-button> + </x-slot> +</x-card> diff --git a/resources/views/settings/users/partials/update-user-info.blade.php b/resources/views/settings/users/partials/update-user-info.blade.php new file mode 100644 index 00000000..eb4e1c50 --- /dev/null +++ b/resources/views/settings/users/partials/update-user-info.blade.php @@ -0,0 +1,99 @@ +<x-card> + <x-slot name="title">User Info</x-slot> + + <x-slot name="description">You can update user's info here</x-slot> + + <form + id="update-user-info" + hx-post="{{ route("settings.users.update", ["user" => $user]) }}" + hx-swap="outerHTML" + hx-select="#update-user-info" + hx-trigger="submit" + hx-ext="disable-element" + hx-disable-element="#btn-save-info" + class="mt-6 space-y-6" + > + @csrf + <div> + <x-input-label for="name" value="Name" /> + <x-text-input + id="name" + name="name" + type="text" + value="{{ old('name', $user->name) }}" + class="mt-1 block w-full" + required + autocomplete="name" + /> + @error("name") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="email" value="Email" /> + <x-text-input + id="email" + name="email" + type="email" + value="{{ old('email', $user->email) }}" + class="mt-1 block w-full" + required + autocomplete="email" + /> + @error("email") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="timezone" value="Timezone" /> + <x-select-input id="timezone" name="timezone" class="mt-1 block w-full" required> + @foreach (timezone_identifiers_list() as $timezone) + <option + value="{{ $timezone }}" + @if(old('timezone', $user->timezone) == $timezone) selected @endif + > + {{ $timezone }} + </option> + @endforeach + </x-select-input> + @error("timezone") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="role" value="Role" /> + <x-select-input id="role" name="role" class="mt-1 w-full"> + <option + value="{{ \App\Enums\UserRole::USER }}" + @if(old('role', $user->role) === \App\Enums\UserRole::USER) selected @endif + > + User + </option> + <option + value="{{ \App\Enums\UserRole::ADMIN }}" + @if(old('role', $user->role) === \App\Enums\UserRole::ADMIN) selected @endif + > + Admin + </option> + </x-select-input> + @error("role") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="password" value="New Password" /> + <x-text-input id="password" name="password" type="password" class="mt-1 w-full" /> + @error("password") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + </form> + + <x-slot name="actions"> + <x-primary-button id="btn-save-info" form="update-user-info">Save</x-primary-button> + </x-slot> +</x-card> diff --git a/resources/views/settings/users/partials/update-user-password.blade.php b/resources/views/settings/users/partials/update-user-password.blade.php new file mode 100644 index 00000000..b1b80e9c --- /dev/null +++ b/resources/views/settings/users/partials/update-user-password.blade.php @@ -0,0 +1,69 @@ +<x-card> + <x-slot name="title"> + {{ __("Update Password") }} + </x-slot> + + <x-slot name="description"> + {{ __("Ensure your account is using a long, random password to stay secure.") }} + </x-slot> + + <form + id="update-password" + class="mt-6 space-y-6" + hx-post="{{ route("profile.password") }}" + hx-swap="outerHTML" + hx-select="#update-password" + hx-trigger="submit" + hx-ext="disable-element" + hx-disable-element="#btn-save-password" + > + @csrf + + <div> + <x-input-label for="current_password" :value="__('Current Password')" /> + <x-text-input + id="current_password" + name="current_password" + type="password" + class="mt-1 block w-full" + autocomplete="current-password" + /> + @error("current_password") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="password" :value="__('New Password')" /> + <x-text-input + id="password" + name="password" + type="password" + class="mt-1 block w-full" + autocomplete="new-password" + /> + @error("password") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + + <div> + <x-input-label for="password_confirmation" :value="__('Confirm Password')" /> + <x-text-input + id="password_confirmation" + name="password_confirmation" + type="password" + class="mt-1 block w-full" + autocomplete="new-password" + /> + @error("password_confirmation") + <x-input-error class="mt-2" :messages="$message" /> + @enderror + </div> + </form> + <x-slot name="actions"> + <x-primary-button id="btn-save-password" form="update-password"> + {{ __("Save") }} + </x-primary-button> + </x-slot> +</x-card> diff --git a/resources/views/settings/users/show.blade.php b/resources/views/settings/users/show.blade.php new file mode 100644 index 00000000..bb995adc --- /dev/null +++ b/resources/views/settings/users/show.blade.php @@ -0,0 +1,9 @@ +<x-settings-layout> + <x-slot name="pageTitle">Users</x-slot> + + <x-container> + @include("settings.users.partials.update-projects") + + @include("settings.users.partials.update-user-info") + </x-container> +</x-settings-layout> diff --git a/resources/views/sites/partials/create/fields/source-control.blade.php b/resources/views/sites/partials/create/fields/source-control.blade.php index 7bbb8443..515d557d 100644 --- a/resources/views/sites/partials/create/fields/source-control.blade.php +++ b/resources/views/sites/partials/create/fields/source-control.blade.php @@ -13,7 +13,10 @@ </option> @endforeach </x-select-input> - <x-secondary-button :href="route('source-controls', ['redirect' => request()->url()])" class="ml-2 flex-none"> + <x-secondary-button + :href="route('settings.source-controls', ['redirect' => request()->url()])" + class="ml-2 flex-none" + > {{ __("Connect") }} </x-secondary-button> </div> diff --git a/routes/settings.php b/routes/settings.php index b548e61c..7b700480 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -1,66 +1,62 @@ <?php use App\Http\Controllers\Settings\NotificationChannelController; -use App\Http\Controllers\Settings\ProfileController; use App\Http\Controllers\Settings\ProjectController; use App\Http\Controllers\Settings\ServerProviderController; use App\Http\Controllers\Settings\SourceControlController; use App\Http\Controllers\Settings\SSHKeyController; use App\Http\Controllers\Settings\StorageProviderController; +use App\Http\Controllers\Settings\UserController; use Illuminate\Support\Facades\Route; -// profile -Route::prefix('settings/profile')->group(function () { - Route::get('/', [ProfileController::class, 'index'])->name('profile'); - Route::post('info', [ProfileController::class, 'info'])->name('profile.info'); - Route::post('password', [ProfileController::class, 'password'])->name('profile.password'); +Route::prefix('settings/users')->group(function () { + Route::get('/', [UserController::class, 'index'])->name('settings.users.index'); + Route::post('/', [UserController::class, 'store'])->name('settings.users.store'); + Route::get('/{user}', [UserController::class, 'show'])->name('settings.users.show'); + Route::post('/{user}', [UserController::class, 'update'])->name('settings.users.update'); + Route::post('/{user}/projects', [UserController::class, 'updateProjects'])->name('settings.users.update-projects'); + Route::delete('/{user}', [UserController::class, 'destroy'])->name('settings.users.delete'); }); -// profile +// projects Route::prefix('settings/projects')->group(function () { - Route::get('/', [ProjectController::class, 'index'])->name('projects'); - Route::post('create', [ProjectController::class, 'create'])->name('projects.create'); - Route::post('update/{project}', [ProjectController::class, 'update'])->name('projects.update'); - Route::delete('delete/{project}', [ProjectController::class, 'delete'])->name('projects.delete'); - Route::get('switch/{project}', [ProjectController::class, 'switch'])->name('projects.switch'); + Route::get('/', [ProjectController::class, 'index'])->name('settings.projects'); + Route::post('create', [ProjectController::class, 'create'])->name('settings.projects.create'); + Route::post('update/{project}', [ProjectController::class, 'update'])->name('settings.projects.update'); + Route::delete('delete/{project}', [ProjectController::class, 'delete'])->name('settings.projects.delete'); }); // server-providers Route::prefix('settings/server-providers')->group(function () { - Route::get('/', [ServerProviderController::class, 'index'])->name('server-providers'); - Route::post('connect', [ServerProviderController::class, 'connect'])->name('server-providers.connect'); - Route::delete('delete/{serverProvider}', [ServerProviderController::class, 'delete']) - ->name('server-providers.delete'); + Route::get('/', [ServerProviderController::class, 'index'])->name('settings.server-providers'); + Route::post('connect', [ServerProviderController::class, 'connect'])->name('settings.server-providers.connect'); + Route::delete('delete/{serverProvider}', [ServerProviderController::class, 'delete'])->name('settings.server-providers.delete'); }); // source-controls Route::prefix('settings/source-controls')->group(function () { - Route::get('/', [SourceControlController::class, 'index'])->name('source-controls'); - Route::post('connect', [SourceControlController::class, 'connect'])->name('source-controls.connect'); - Route::delete('delete/{sourceControl}', [SourceControlController::class, 'delete']) - ->name('source-controls.delete'); + Route::get('/', [SourceControlController::class, 'index'])->name('settings.source-controls'); + Route::post('connect', [SourceControlController::class, 'connect'])->name('settings.source-controls.connect'); + Route::delete('delete/{sourceControl}', [SourceControlController::class, 'delete'])->name('settings.source-controls.delete'); }); // storage-providers Route::prefix('settings/storage-providers')->group(function () { - Route::get('/', [StorageProviderController::class, 'index'])->name('storage-providers'); - Route::post('connect', [StorageProviderController::class, 'connect'])->name('storage-providers.connect'); - Route::delete('delete/{storageProvider}', [StorageProviderController::class, 'delete']) - ->name('storage-providers.delete'); + Route::get('/', [StorageProviderController::class, 'index'])->name('settings.storage-providers'); + Route::post('connect', [StorageProviderController::class, 'connect'])->name('settings.storage-providers.connect'); + Route::delete('delete/{storageProvider}', [StorageProviderController::class, 'delete'])->name('settings.storage-providers.delete'); }); // notification-channels Route::prefix('settings/notification-channels')->group(function () { - Route::get('/', [NotificationChannelController::class, 'index'])->name('notification-channels'); - Route::post('add', [NotificationChannelController::class, 'add']) - ->name('notification-channels.add'); - Route::delete('delete/{id}', [NotificationChannelController::class, 'delete']) - ->name('notification-channels.delete'); + Route::get('/', [NotificationChannelController::class, 'index'])->name('settings.notification-channels'); + Route::post('add', [NotificationChannelController::class, 'add'])->name('settings.notification-channels.add'); + Route::delete('delete/{id}', [NotificationChannelController::class, 'delete'])->name('settings.notification-channels.delete'); }); // ssh-keys Route::prefix('settings/ssh-keys')->group(function () { - Route::get('/', [SSHKeyController::class, 'index'])->name('ssh-keys'); - Route::post('add', [SshKeyController::class, 'add'])->name('ssh-keys.add'); - Route::delete('delete/{id}', [SshKeyController::class, 'delete'])->name('ssh-keys.delete'); + Route::get('/', [SSHKeyController::class, 'index'])->name('settings.ssh-keys'); + Route::post('add', [SshKeyController::class, 'add'])->name('settings.ssh-keys.add'); + Route::delete('delete/{id}', [SshKeyController::class, 'delete'])->name('settings.ssh-keys.delete'); }); diff --git a/routes/web.php b/routes/web.php index 7c056061..7326a48f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,8 @@ <?php +use App\Http\Controllers\ProfileController; use App\Http\Controllers\SearchController; +use App\Http\Controllers\Settings\ProjectController; use Illuminate\Support\Facades\Route; Route::get('/', function () { @@ -8,7 +10,19 @@ }); Route::middleware('auth')->group(function () { - require __DIR__.'/settings.php'; + // profile + Route::prefix('profile')->group(function () { + Route::get('/', [ProfileController::class, 'index'])->name('profile'); + Route::post('info', [ProfileController::class, 'info'])->name('profile.info'); + Route::post('password', [ProfileController::class, 'password'])->name('profile.password'); + }); + + // switch project + Route::get('settings/projects/switch/{project}', [ProjectController::class, 'switch'])->name('settings.projects.switch'); + + Route::middleware('is-admin')->group(function () { + require __DIR__.'/settings.php'; + }); Route::prefix('/servers')->group(function () { require __DIR__.'/server.php'; diff --git a/tests/Feature/NotificationChannelsTest.php b/tests/Feature/NotificationChannelsTest.php index 18c8c982..28f7e5b6 100644 --- a/tests/Feature/NotificationChannelsTest.php +++ b/tests/Feature/NotificationChannelsTest.php @@ -17,7 +17,7 @@ public function test_add_email_channel(): void { $this->actingAs($this->user); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::EMAIL, 'email' => 'email@example.com', 'label' => 'Email', @@ -40,7 +40,7 @@ public function test_cannot_add_email_channel(): void $this->actingAs($this->user); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::EMAIL, 'email' => 'email@example.com', 'label' => 'Email', @@ -61,7 +61,7 @@ public function test_add_slack_channel(): void Http::fake(); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::SLACK, 'webhook_url' => 'https://hooks.slack.com/services/123/token', 'label' => 'Slack', @@ -84,7 +84,7 @@ public function test_cannot_add_slack_channel(): void 'slack.com/*' => Http::response(['ok' => false], 401), ]); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::SLACK, 'webhook_url' => 'https://hooks.slack.com/services/123/token', 'label' => 'Slack', @@ -104,7 +104,7 @@ public function test_add_discord_channel(): void Http::fake(); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::DISCORD, 'webhook_url' => 'https://discord.com/api/webhooks/123/token', 'label' => 'Discord', @@ -127,7 +127,7 @@ public function test_cannot_add_discord_channel(): void 'discord.com/*' => Http::response(['ok' => false], 401), ]); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::DISCORD, 'webhook_url' => 'https://discord.com/api/webhooks/123/token', 'label' => 'Discord', @@ -147,7 +147,7 @@ public function test_add_telegram_channel(): void Http::fake(); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::TELEGRAM, 'bot_token' => 'token', 'chat_id' => '123', @@ -172,7 +172,7 @@ public function test_cannot_add_telegram_channel(): void 'api.telegram.org/*' => Http::response(['ok' => false], 401), ]); - $this->post(route('notification-channels.add'), [ + $this->post(route('settings.notification-channels.add'), [ 'provider' => NotificationChannel::TELEGRAM, 'bot_token' => 'token', 'chat_id' => '123', @@ -193,7 +193,7 @@ public function test_see_channels_list(): void $channel = \App\Models\NotificationChannel::factory()->create(); - $this->get(route('notification-channels')) + $this->get(route('settings.notification-channels')) ->assertSuccessful() ->assertSee($channel->provider); } @@ -204,7 +204,7 @@ public function test_delete_channel(): void $channel = \App\Models\NotificationChannel::factory()->create(); - $this->delete(route('notification-channels.delete', $channel->id)) + $this->delete(route('settings.notification-channels.delete', $channel->id)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('notification_channels', [ diff --git a/tests/Feature/ProjectsTest.php b/tests/Feature/ProjectsTest.php index 6b60d3e6..aa375684 100644 --- a/tests/Feature/ProjectsTest.php +++ b/tests/Feature/ProjectsTest.php @@ -14,7 +14,7 @@ public function test_create_project(): void { $this->actingAs($this->user); - $this->post(route('projects.create'), [ + $this->post(route('settings.projects.create'), [ 'name' => 'test', ])->assertSessionDoesntHaveErrors(); @@ -27,11 +27,11 @@ public function test_see_projects_list(): void { $this->actingAs($this->user); - $project = Project::factory()->create([ - 'user_id' => $this->user->id, - ]); + $project = Project::factory()->create(); - $this->get(route('projects')) + $this->user->projects()->attach($project); + + $this->get(route('settings.projects')) ->assertSuccessful() ->assertSee($project->name); } @@ -40,11 +40,11 @@ public function test_delete_project(): void { $this->actingAs($this->user); - $project = Project::factory()->create([ - 'user_id' => $this->user->id, - ]); + $project = Project::factory()->create(); - $this->delete(route('projects.delete', $project)) + $this->user->projects()->attach($project); + + $this->delete(route('settings.projects.delete', $project)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('projects', [ @@ -56,11 +56,11 @@ public function test_edit_project(): void { $this->actingAs($this->user); - $project = Project::factory()->create([ - 'user_id' => $this->user->id, - ]); + $project = Project::factory()->create(); - $this->post(route('projects.update', $project), [ + $this->user->projects()->attach($project); + + $this->post(route('settings.projects.update', $project), [ 'name' => 'new-name', ])->assertSessionDoesntHaveErrors(); @@ -74,7 +74,7 @@ public function test_cannot_delete_last_project(): void { $this->actingAs($this->user); - $this->delete(route('projects.delete', [ + $this->delete(route('settings.projects.delete', [ 'project' => $this->user->currentProject, ])) ->assertSessionDoesntHaveErrors() diff --git a/tests/Feature/ServerProvidersTest.php b/tests/Feature/ServerProvidersTest.php index 85b5b75e..0f99abdc 100644 --- a/tests/Feature/ServerProvidersTest.php +++ b/tests/Feature/ServerProvidersTest.php @@ -27,7 +27,7 @@ public function test_connect_provider(string $provider, array $input): void ], $input ); - $this->post(route('server-providers.connect'), $data)->assertSessionDoesntHaveErrors(); + $this->post(route('settings.server-providers.connect'), $data)->assertSessionDoesntHaveErrors(); $this->assertDatabaseHas('server_providers', [ 'provider' => $provider, @@ -53,7 +53,7 @@ public function test_cannot_connect_to_provider(string $provider, array $input): ], $input ); - $this->post(route('server-providers.connect'), $data)->assertSessionHasErrors(); + $this->post(route('settings.server-providers.connect'), $data)->assertSessionHasErrors(); $this->assertDatabaseMissing('server_providers', [ 'provider' => $provider, @@ -69,7 +69,7 @@ public function test_see_providers_list(): void 'user_id' => $this->user->id, ]); - $this->get(route('server-providers')) + $this->get(route('settings.server-providers')) ->assertSuccessful() ->assertSee($provider->profile); } @@ -86,7 +86,7 @@ public function test_delete_provider(string $provider): void 'provider' => $provider, ]); - $this->delete(route('server-providers.delete', $provider)) + $this->delete(route('settings.server-providers.delete', $provider)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('server_providers', [ @@ -110,7 +110,7 @@ public function test_cannot_delete_provider(string $provider): void 'provider_id' => $provider->id, ]); - $this->delete(route('server-providers.delete', $provider)) + $this->delete(route('settings.server-providers.delete', $provider)) ->assertSessionDoesntHaveErrors() ->assertSessionHas('toast.type', 'error') ->assertSessionHas('toast.message', 'This server provider is being used by a server.'); diff --git a/tests/Feature/SourceControlsTest.php b/tests/Feature/SourceControlsTest.php index cca5f088..067263ad 100644 --- a/tests/Feature/SourceControlsTest.php +++ b/tests/Feature/SourceControlsTest.php @@ -28,7 +28,7 @@ public function test_connect_provider(string $provider, ?string $customUrl, arra if ($customUrl !== null) { $input['url'] = $customUrl; } - $this->post(route('source-controls.connect'), $input) + $this->post(route('settings.source-controls.connect'), $input) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseHas('source_controls', [ @@ -50,7 +50,7 @@ public function test_delete_provider(string $provider): void 'profile' => 'test', ]); - $this->delete(route('source-controls.delete', $sourceControl->id)) + $this->delete(route('settings.source-controls.delete', $sourceControl->id)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('source_controls', [ @@ -75,7 +75,7 @@ public function test_cannot_delete_provider(string $provider): void 'source_control_id' => $sourceControl->id, ]); - $this->delete(route('source-controls.delete', $sourceControl->id)) + $this->delete(route('settings.source-controls.delete', $sourceControl->id)) ->assertSessionDoesntHaveErrors() ->assertSessionHas('toast.type', 'error') ->assertSessionHas('toast.message', 'This source control is being used by a site.'); diff --git a/tests/Feature/SshKeysTest.php b/tests/Feature/SshKeysTest.php index 154d542c..81b1fd66 100644 --- a/tests/Feature/SshKeysTest.php +++ b/tests/Feature/SshKeysTest.php @@ -14,7 +14,7 @@ public function test_create_ssh_key(): void { $this->actingAs($this->user); - $this->post(route('ssh-keys.add'), [ + $this->post(route('settings.ssh-keys.add'), [ 'name' => 'test', 'public_key' => 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== test@test.local', ])->assertSessionDoesntHaveErrors(); @@ -28,7 +28,7 @@ public function test_get_public_keys_list(): void 'user_id' => $this->user->id, ]); - $this->get(route('ssh-keys')) + $this->get(route('settings.ssh-keys')) ->assertSuccessful() ->assertSee($key->name); } @@ -41,7 +41,7 @@ public function test_delete_key(): void 'user_id' => $this->user->id, ]); - $this->delete(route('ssh-keys.delete', $key->id)) + $this->delete(route('settings.ssh-keys.delete', $key->id)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('ssh_keys', [ diff --git a/tests/Feature/StorageProvidersTest.php b/tests/Feature/StorageProvidersTest.php index a4577f93..2b4d687a 100644 --- a/tests/Feature/StorageProvidersTest.php +++ b/tests/Feature/StorageProvidersTest.php @@ -19,7 +19,7 @@ public function test_connect_dropbox(): void Http::fake(); - $this->post(route('storage-providers.connect'), [ + $this->post(route('settings.storage-providers.connect'), [ 'provider' => StorageProvider::DROPBOX, 'name' => 'profile', 'token' => 'token', @@ -40,7 +40,7 @@ public function test_see_providers_list(): void 'provider' => StorageProvider::DROPBOX, ]); - $this->get(route('storage-providers')) + $this->get(route('settings.storage-providers')) ->assertSuccessful() ->assertSee($provider->profile); } @@ -53,7 +53,7 @@ public function test_delete_provider(): void 'user_id' => $this->user->id, ]); - $this->delete(route('storage-providers.delete', $provider->id)) + $this->delete(route('settings.storage-providers.delete', $provider->id)) ->assertSessionDoesntHaveErrors(); $this->assertDatabaseMissing('storage_providers', [ @@ -79,7 +79,7 @@ public function test_cannot_delete_provider(): void 'storage_id' => $provider->id, ]); - $this->delete(route('storage-providers.delete', $provider->id)) + $this->delete(route('settings.storage-providers.delete', $provider->id)) ->assertSessionDoesntHaveErrors() ->assertSessionHas('toast.type', 'error') ->assertSessionHas('toast.message', 'This storage provider is being used by a backup.'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 0a64bb33..1fb04ef6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,7 @@ use App\Enums\Database; use App\Enums\NotificationChannel; use App\Enums\ServiceStatus; +use App\Enums\UserRole; use App\Enums\Webserver; use App\Models\Server; use App\Models\Site; @@ -30,7 +31,9 @@ public function setUp(): void config()->set('queue.connections.ssh.driver', 'sync'); config()->set('filesystems.disks.key-pairs.root', storage_path('app/key-pairs-test')); - $this->user = User::factory()->create(); + $this->user = User::factory()->create([ + 'role' => UserRole::ADMIN, + ]); $this->user->createDefaultProject(); \App\Models\NotificationChannel::factory()->create([ diff --git a/tests/Unit/Commands/CreateUserCommandTest.php b/tests/Unit/Commands/CreateUserCommandTest.php index 45e0664d..2979e5f9 100644 --- a/tests/Unit/Commands/CreateUserCommandTest.php +++ b/tests/Unit/Commands/CreateUserCommandTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Commands; +use App\Models\Project; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -27,7 +28,36 @@ public function test_create_user(): void $user = User::query()->where('email', 'john@doe.com')->first(); $this->assertDatabaseHas('projects', [ + 'name' => 'default', + ]); + } + + public function test_create_user_and_project(): void + { + Project::query()->delete(); + User::query()->delete(); + + $this->artisan('user:create', [ + 'name' => 'John Doe', + 'email' => 'john@doe.com', + 'password' => 'password', + ])->expectsOutput('User created!'); + + $this->assertDatabaseHas('users', [ + 'name' => 'John Doe', + 'email' => 'john@doe.com', + ]); + + /** @var User $user */ + $user = User::query()->where('email', 'john@doe.com')->first(); + + $this->assertDatabaseHas('projects', [ + 'name' => 'default', + ]); + + $this->assertDatabaseHas('user_project', [ 'user_id' => $user->id, + 'project_id' => $user->refresh()->current_project_id, ]); }