From d846acaa8d8ed33f96281fb462a409ed831c279c Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:58:04 +0200 Subject: [PATCH] User management (#185) --- app/Actions/Projects/CreateProject.php | 7 +- app/Actions/User/CreateUser.php | 40 ++++++ app/Actions/User/UpdateUser.php | 48 +++++++ app/Console/Commands/CreateUserCommand.php | 3 +- app/Enums/UserRole.php | 10 ++ .../Controllers/ApplicationController.php | 16 +++ app/Http/Controllers/ConsoleController.php | 4 + app/Http/Controllers/CronjobController.php | 6 + .../Controllers/DatabaseBackupController.php | 12 ++ app/Http/Controllers/DatabaseController.php | 6 + .../Controllers/DatabaseUserController.php | 8 ++ app/Http/Controllers/FirewallController.php | 6 + app/Http/Controllers/MetricController.php | 6 + app/Http/Controllers/PHPController.php | 14 ++ .../{Settings => }/ProfileController.php | 5 +- app/Http/Controllers/QueueController.php | 10 ++ app/Http/Controllers/SSHKeyController.php | 8 ++ app/Http/Controllers/SSLController.php | 6 + app/Http/Controllers/SearchController.php | 13 ++ app/Http/Controllers/ServerController.php | 19 ++- app/Http/Controllers/ServerLogController.php | 10 ++ .../Controllers/ServerSettingController.php | 8 ++ app/Http/Controllers/ServiceController.php | 16 +++ .../NotificationChannelController.php | 4 +- .../Settings/ProjectController.php | 8 +- .../Controllers/Settings/SSHKeyController.php | 4 +- .../Settings/ServerProviderController.php | 2 +- .../Settings/SourceControlController.php | 4 +- .../Settings/StorageProviderController.php | 2 +- .../Controllers/Settings/UserController.php | 78 +++++++++++ app/Http/Controllers/SiteController.php | 12 ++ app/Http/Controllers/SiteLogController.php | 2 + .../Controllers/SiteSettingController.php | 10 ++ app/Http/Kernel.php | 1 + app/Http/Middleware/IsAdmin.php | 25 ++++ app/Models/Project.php | 6 + app/Models/User.php | 22 +++- app/NotificationChannels/Discord.php | 2 +- app/NotificationChannels/Slack.php | 2 +- app/Policies/ProjectPolicy.php | 35 +++++ app/Policies/ServerPolicy.php | 41 ++++++ .../{ProfileLayout.php => SettingsLayout.php} | 4 +- database/factories/ProjectFactory.php | 1 - database/factories/ServerFactory.php | 5 - database/factories/UserFactory.php | 2 + ..._095440_update_storage_providers_table.php | 14 +- ...4_04_24_213204_add_role_to_users_table.php | 31 +++++ ...04_26_122230_create_user_project_table.php | 38 ++++++ ...23326_drop_user_id_from_projects_table.php | 28 ++++ resources/views/components/dropdown.blade.php | 31 ++++- .../heroicons/o-arrow-left-circle.blade.php | 14 ++ .../heroicons/o-user-group.blade.php | 14 ++ .../components/heroicons/o-x-mark.blade.php | 10 ++ resources/views/components/select2.blade.php | 0 resources/views/components/status.blade.php | 10 +- .../views/components/user-dropdown.blade.php | 12 +- .../partials/create-backup-modal.blade.php | 2 +- resources/views/layouts/navigation.blade.php | 12 +- .../layouts/partials/project-select.blade.php | 27 ++-- .../layouts/partials/server-select.blade.php | 68 +++++----- .../layouts/partials/site-select.blade.php | 124 ------------------ .../{profile.blade.php => settings.blade.php} | 0 resources/views/layouts/sidebar.blade.php | 107 +++++++++------ resources/views/profile/index.blade.php | 9 ++ .../two-factor-authentication.blade.php | 78 +++++++++++ .../partials/update-password.blade.php | 0 .../update-profile-information.blade.php | 0 .../servers/partials/create-server.blade.php | 2 +- .../notification-channels/index.blade.php | 4 +- .../partials/add-channel.blade.php | 2 +- .../partials/channels-list.blade.php | 2 +- .../views/settings/profile/index.blade.php | 9 -- .../two-factor-authentication.blade.php | 60 --------- .../views/settings/projects/index.blade.php | 4 +- .../partials/create-project.blade.php | 2 +- .../projects/partials/edit-project.blade.php | 2 +- .../projects/partials/projects-list.blade.php | 2 +- .../settings/server-providers/index.blade.php | 4 +- .../partials/connect-provider.blade.php | 2 +- .../partials/providers-list.blade.php | 2 +- .../settings/source-controls/index.blade.php | 4 +- .../partials/connect.blade.php | 2 +- .../partials/source-controls-list.blade.php | 2 +- .../views/settings/ssh-keys/index.blade.php | 4 +- .../ssh-keys/partials/add-key.blade.php | 2 +- .../ssh-keys/partials/keys-list.blade.php | 2 +- .../storage-providers/index.blade.php | 4 +- .../partials/connect-provider.blade.php | 2 +- .../partials/providers-list.blade.php | 2 +- .../views/settings/users/index.blade.php | 61 +++++++++ .../users/partials/create-user.blade.php | 69 ++++++++++ .../users/partials/update-projects.blade.php | 120 +++++++++++++++++ .../users/partials/update-user-info.blade.php | 99 ++++++++++++++ .../partials/update-user-password.blade.php | 69 ++++++++++ resources/views/settings/users/show.blade.php | 9 ++ .../create/fields/source-control.blade.php | 5 +- routes/settings.php | 60 ++++----- routes/web.php | 16 ++- tests/Feature/NotificationChannelsTest.php | 20 +-- tests/Feature/ProjectsTest.php | 28 ++-- tests/Feature/ServerProvidersTest.php | 10 +- tests/Feature/SourceControlsTest.php | 6 +- tests/Feature/SshKeysTest.php | 6 +- tests/Feature/StorageProvidersTest.php | 8 +- tests/TestCase.php | 5 +- tests/Unit/Commands/CreateUserCommandTest.php | 30 +++++ 106 files changed, 1490 insertions(+), 434 deletions(-) create mode 100644 app/Actions/User/CreateUser.php create mode 100644 app/Actions/User/UpdateUser.php create mode 100644 app/Enums/UserRole.php rename app/Http/Controllers/{Settings => }/ProfileController.php (87%) create mode 100644 app/Http/Controllers/Settings/UserController.php create mode 100644 app/Http/Middleware/IsAdmin.php create mode 100644 app/Policies/ProjectPolicy.php create mode 100644 app/Policies/ServerPolicy.php rename app/View/Components/{ProfileLayout.php => SettingsLayout.php} (66%) create mode 100644 database/migrations/2024_04_24_213204_add_role_to_users_table.php create mode 100644 database/migrations/2024_04_26_122230_create_user_project_table.php create mode 100644 database/migrations/2024_04_26_123326_drop_user_id_from_projects_table.php create mode 100644 resources/views/components/heroicons/o-arrow-left-circle.blade.php create mode 100644 resources/views/components/heroicons/o-user-group.blade.php create mode 100644 resources/views/components/heroicons/o-x-mark.blade.php create mode 100644 resources/views/components/select2.blade.php delete mode 100644 resources/views/layouts/partials/site-select.blade.php rename resources/views/layouts/{profile.blade.php => settings.blade.php} (100%) create mode 100644 resources/views/profile/index.blade.php create mode 100644 resources/views/profile/partials/two-factor-authentication.blade.php rename resources/views/{settings => }/profile/partials/update-password.blade.php (100%) rename resources/views/{settings => }/profile/partials/update-profile-information.blade.php (100%) delete mode 100644 resources/views/settings/profile/index.blade.php delete mode 100644 resources/views/settings/profile/partials/two-factor-authentication.blade.php create mode 100644 resources/views/settings/users/index.blade.php create mode 100644 resources/views/settings/users/partials/create-user.blade.php create mode 100644 resources/views/settings/users/partials/update-projects.blade.php create mode 100644 resources/views/settings/users/partials/update-user-info.blade.php create mode 100644 resources/views/settings/users/partials/update-user-password.blade.php create mode 100644 resources/views/settings/users/show.blade.php diff --git a/app/Actions/Projects/CreateProject.php b/app/Actions/Projects/CreateProject.php index 839f120..45c4137 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 0000000..516de09 --- /dev/null +++ b/app/Actions/User/CreateUser.php @@ -0,0 +1,40 @@ +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 0000000..70fc0c5 --- /dev/null +++ b/app/Actions/User/UpdateUser.php @@ -0,0 +1,48 @@ +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 187431b..c3bd6ee 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 0000000..57fc7eb --- /dev/null +++ b/app/Enums/UserRole.php @@ -0,0 +1,10 @@ +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 51639f1..6905858 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 2c55c3c..e958d05 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 7ab6367..770cbb2 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 dafd84f..4b5c946 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 945a908..939dffd 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 218bb85..d52fecc 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 6d83190..060f96c 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 492ed97..07bf6db 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 4476c6a..cd49a35 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -1,11 +1,10 @@ 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 4d7f0f5..c576ebd 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 18d7d52..8523aad 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 712d8e7..0c23e4b 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 0e6e67f..f6bc4aa 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 933c393..00c6764 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 15f689e..a89ae60 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 df4967b..3d9f03d 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 88e1223..c007c4f 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 3d1f5c2..eb8cc90 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 5fc1495..181b827 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 bae2092..9f738a5 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 f3bb097..019b20f 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 939c56b..fc5b12b 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 0000000..9d40773 --- /dev/null +++ b/app/Http/Controllers/Settings/UserController.php @@ -0,0 +1,78 @@ +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 1ad175d..f99862c 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 79c89b0..2975a99 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 b45170e..a17ac4d 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 f0394da..a173119 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 0000000..c5b4697 --- /dev/null +++ b/app/Http/Middleware/IsAdmin.php @@ -0,0 +1,25 @@ +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 1985d12..d354817 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 55aea29..efe30c1 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 $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 3655adf..ce283fd 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 b4a68ff..3bc28cd 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 0000000..7c062da --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,35 @@ +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 0000000..2f9eec2 --- /dev/null +++ b/app/Policies/ServerPolicy.php @@ -0,0 +1,41 @@ +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 b4f6c0e..3cc5442 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 d99018a..239a96a 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 7219327..7bc0511 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 d202209..26cb238 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 200140e..b5dc1ff 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 0000000..2fdde00 --- /dev/null +++ b/database/migrations/2024_04_24_213204_add_role_to_users_table.php @@ -0,0 +1,31 @@ +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 0000000..60820d9 --- /dev/null +++ b/database/migrations/2024_04_26_122230_create_user_project_table.php @@ -0,0 +1,38 @@ +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 0000000..5c6c173 --- /dev/null +++ b/database/migrations/2024_04_26_123326_drop_user_id_from_projects_table.php @@ -0,0 +1,28 @@ +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 dbd0f62..84bca4c 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" >
+ @if ($search) +
+ +
+ @endif + {{ $content }}
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 0000000..a9da3c4 --- /dev/null +++ b/resources/views/components/heroicons/o-arrow-left-circle.blade.php @@ -0,0 +1,14 @@ + + + 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 0000000..87da155 --- /dev/null +++ b/resources/views/components/heroicons/o-user-group.blade.php @@ -0,0 +1,14 @@ + + + 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 0000000..11d6f99 --- /dev/null +++ b/resources/views/components/heroicons/o-x-mark.blade.php @@ -0,0 +1,10 @@ + + + diff --git a/resources/views/components/select2.blade.php b/resources/views/components/select2.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/components/status.blade.php b/resources/views/components/status.blade.php index 2ef5f14..70f363d 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 e78559f..a50c5fd 100644 --- a/resources/views/components/user-dropdown.blade.php +++ b/resources/views/components/user-dropdown.blade.php @@ -9,22 +9,22 @@ {{ __("Profile") }} - + {{ __("Projects") }} - + {{ __("Server Providers") }} - + {{ __("Source Controls") }} - + {{ __("Storage Providers") }} - + {{ __("Notification Channels") }} - + {{ __("SSH Keys") }} diff --git a/resources/views/databases/partials/create-backup-modal.blade.php b/resources/views/databases/partials/create-backup-modal.blade.php index 45e148a..6fc1782 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" @endforeach - + Connect diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 779fff6..9044f47 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 >
-
+
@@ -17,7 +17,7 @@ class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gr Open sidebar - +
@@ -70,9 +70,11 @@ class="flex rounded-full p-1 text-sm focus:ring-2 focus:ring-gray-300 dark:focus {{ __("Profile") }} - - {{ __("Projects") }} - + @if (auth()->user()->isAdmin()) + + {{ __("Projects") }} + + @endif
diff --git a/resources/views/layouts/partials/project-select.blade.php b/resources/views/layouts/partials/project-select.blade.php index 8e1aebb..42298f6 100644 --- a/resources/views/layouts/partials/project-select.blade.php +++ b/resources/views/layouts/partials/project-select.blade.php @@ -5,7 +5,14 @@
- {{ auth()->user()->currentProject?->name ?? __("Select Project") }} + + @@ -14,8 +21,8 @@ class="flex h-10 w-max items-center rounded-md border border-gray-200 bg-gray-10 @foreach (auth()->user()->projects as $project) - - {{ ucfirst($project->name) }} + + {{ $project->name }} @if ($project->id == auth()->user()->current_project_id) @@ -24,12 +31,14 @@ class="flex h-10 w-max items-center rounded-md border border-gray-200 bg-gray-10 @endforeach - - {{ __("Projects List") }} - - - {{ __("Create a Project") }} - + @if (auth()->user()->isAdmin()) + + {{ __("Projects List") }} + + + {{ __("Create a Project") }} + + @endif
diff --git a/resources/views/layouts/partials/server-select.blade.php b/resources/views/layouts/partials/server-select.blade.php index 3087f78..6ab99e4 100644 --- a/resources/views/layouts/partials/server-select.blade.php +++ b/resources/views/layouts/partials/server-select.blade.php @@ -1,35 +1,37 @@ -
- - -
-
- {{ isset($server) ? $server->name : "Select Server" }} +@if (auth()->user()->currentProject &&auth()->user()->can("view", auth()->user()->currentProject)) +
+ + +
+
+ {{ isset($server) ? $server->name : "Select Server" }} +
+
- -
- - - @foreach (auth()->user()->currentProject->servers as $s) - - {{ ucfirst($s->name) }} - @if (isset($server) && $server->id == $s->id) - - - - @endif - - @endforeach + + + @foreach (auth()->user()->currentProject->servers as $s) + + {{ ucfirst($s->name) }} + @if (isset($server) && $server->id == $s->id) + + + + @endif + + @endforeach - - {{ __("Servers List") }} - - - {{ __("Create a Server") }} - - - -
+ + {{ __("Servers List") }} + + + {{ __("Create a Server") }} + + + +
+@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 83a2410..0000000 --- a/resources/views/layouts/partials/site-select.blade.php +++ /dev/null @@ -1,124 +0,0 @@ -
- - 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 398c27d..56f4c50 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 > - {{ __("Settings") }} + {{ __("Server Settings") }} @@ -168,45 +168,72 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g Profile -
  • - - - Projects - -
  • -
  • - - - Server Providers - -
  • -
  • - - - Source Controls - -
  • -
  • - - - Storage Providers - -
  • -
  • - - - Notification Channels - -
  • -
  • - - - SSH Keys - -
  • + + @if (auth()->user()->isAdmin()) +
  • + + + Users + +
  • +
  • + + + Projects + +
  • +
  • + + + Server Providers + +
  • +
  • + + + Source Controls + +
  • +
  • + + + Storage Providers + +
  • +
  • + + + Notification Channels + +
  • +
  • + + + SSH Keys + +
  • + @endif
    diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php new file mode 100644 index 0000000..ae0e0f2 --- /dev/null +++ b/resources/views/profile/index.blade.php @@ -0,0 +1,9 @@ + + {{ __("Profile") }} + + @include("profile.partials.update-profile-information") + + @include("profile.partials.update-password") + + @include("profile.partials.two-factor-authentication") + 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 0000000..1d44665 --- /dev/null +++ b/resources/views/profile/partials/two-factor-authentication.blade.php @@ -0,0 +1,78 @@ + + + {{ __("Two Factor Authentication") }} + + + + {{ __("Here you can activate 2FA to secure your account") }} + + +
    + @if (! auth()->user()->two_factor_secret) + {{-- Enable 2FA --}} + + @csrf + + + {{ __("Enable Two-Factor") }} + + + @else + {{-- Disable 2FA --}} +
    + @csrf + @method("DELETE") + + + {{ __("Disable Two-Factor") }} + +
    + + @if (session("status") == "two-factor-authentication-enabled") +
    + {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }} +
    + +
    + {!! auth()->user()->twoFactorQrCodeSvg() !!} +
    + @endif + + {{-- Show 2FA Recovery Codes --}} +
    + {{ __("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.") }} +
    + +
    + @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code) +
    {{ $code }}
    + @endforeach +
    + + {{-- Regenerate 2FA Recovery Codes --}} +
    + @csrf + + + {{ __("Regenerate Recovery Codes") }} + +
    + @endif +
    +
    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 c32b837..5921bf1 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 {{ __("Connect") }} diff --git a/resources/views/settings/notification-channels/index.blade.php b/resources/views/settings/notification-channels/index.blade.php index 3b27ab8..e09d74b 100644 --- a/resources/views/settings/notification-channels/index.blade.php +++ b/resources/views/settings/notification-channels/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("Notification Channels") }} @include("settings.notification-channels.partials.channels-list") - + 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 a137bef..ab413c3 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 @@
    diff --git a/resources/views/settings/profile/index.blade.php b/resources/views/settings/profile/index.blade.php deleted file mode 100644 index 141d25f..0000000 --- a/resources/views/settings/profile/index.blade.php +++ /dev/null @@ -1,9 +0,0 @@ - - {{ __("Profile") }} - - @include("settings.profile.partials.update-profile-information") - - @include("settings.profile.partials.update-password") - - @include("settings.profile.partials.two-factor-authentication") - 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 6ad8d89..0000000 --- a/resources/views/settings/profile/partials/two-factor-authentication.blade.php +++ /dev/null @@ -1,60 +0,0 @@ - - - {{ __("Two Factor Authentication") }} - - - - {{ __("Here you can activate 2FA to secure your account") }} - - - @if (! auth()->user()->two_factor_secret) - {{-- Enable 2FA --}} - - @csrf - - - {{ __("Enable Two-Factor") }} - - - @else - {{-- Disable 2FA --}} -
    - @csrf - @method("DELETE") - - - {{ __("Disable Two-Factor") }} - -
    - - @if (session("status") == "two-factor-authentication-enabled") -
    - {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }} -
    - -
    - {!! auth()->user()->twoFactorQrCodeSvg() !!} -
    - @endif - - {{-- Show 2FA Recovery Codes --}} -
    - {{ __("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.") }} -
    - -
    - @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code) -
    {{ $code }}
    - @endforeach -
    - - {{-- Regenerate 2FA Recovery Codes --}} -
    - @csrf - - - {{ __("Regenerate Recovery Codes") }} - -
    - @endif -
    diff --git a/resources/views/settings/projects/index.blade.php b/resources/views/settings/projects/index.blade.php index 694dc05..bdc4c95 100644 --- a/resources/views/settings/projects/index.blade.php +++ b/resources/views/settings/projects/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("Projects") }} @include("settings.projects.partials.projects-list") - + diff --git a/resources/views/settings/projects/partials/create-project.blade.php b/resources/views/settings/projects/partials/create-project.blade.php index 465bc44..1d0b11f 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 @@
    @include("settings.projects.partials.edit-project", ["project" => $project]) diff --git a/resources/views/settings/server-providers/index.blade.php b/resources/views/settings/server-providers/index.blade.php index 3050e1e..6c0e645 100644 --- a/resources/views/settings/server-providers/index.blade.php +++ b/resources/views/settings/server-providers/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("Storage Providers") }} @include("settings.server-providers.partials.providers-list") - + 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 f52f3ab..012d616 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 @@
    diff --git a/resources/views/settings/source-controls/index.blade.php b/resources/views/settings/source-controls/index.blade.php index 609a387..c4c4d68 100644 --- a/resources/views/settings/source-controls/index.blade.php +++ b/resources/views/settings/source-controls/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("Source Controls") }} @include("settings.source-controls.partials.source-controls-list") - + diff --git a/resources/views/settings/source-controls/partials/connect.blade.php b/resources/views/settings/source-controls/partials/connect.blade.php index b1280e5..edbea6f 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 @@
    diff --git a/resources/views/settings/ssh-keys/index.blade.php b/resources/views/settings/ssh-keys/index.blade.php index 4add01e..e21a1a9 100644 --- a/resources/views/settings/ssh-keys/index.blade.php +++ b/resources/views/settings/ssh-keys/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("SSH Keys") }} @include("settings.ssh-keys.partials.keys-list") - + 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 0405089..54a4d76 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 @@
    diff --git a/resources/views/settings/storage-providers/index.blade.php b/resources/views/settings/storage-providers/index.blade.php index 1c4024f..1be9a73 100644 --- a/resources/views/settings/storage-providers/index.blade.php +++ b/resources/views/settings/storage-providers/index.blade.php @@ -1,5 +1,5 @@ - + {{ __("Storage Providers") }} @include("settings.storage-providers.partials.providers-list") - + 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 81241c1..346c0cb 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 @@
    diff --git a/resources/views/settings/users/index.blade.php b/resources/views/settings/users/index.blade.php new file mode 100644 index 0000000..7593c27 --- /dev/null +++ b/resources/views/settings/users/index.blade.php @@ -0,0 +1,61 @@ + + Users + + + + Users + Here you can manage users + + @include("settings.users.partials.create-user") + + +
    + + + + ID + Name + Email + Role + + + + + @foreach ($users as $user) + + {{ $user->id }} + {{ $user->name }} + {{ $user->email }} + +
    + @if ($user->role === \App\Enums\UserRole::ADMIN) + ADMIN + @else + USER + @endif +
    +
    + + + + + + + + +
    + @endforeach +
    +
    + +
    +
    +
    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 0000000..1a9e15c --- /dev/null +++ b/resources/views/settings/users/partials/create-user.blade.php @@ -0,0 +1,69 @@ +
    + New User + + + + @csrf +

    Create New User

    + +
    + + + @error("name") + + @enderror +
    + +
    + + + @error("email") + + @enderror +
    + +
    + + + @error("password") + + @enderror +
    + +
    + + + + + + @error("role") + + @enderror +
    + +
    + Cancel + + Create +
    + +
    +
    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 0000000..e3aa0f0 --- /dev/null +++ b/resources/views/settings/users/partials/update-projects.blade.php @@ -0,0 +1,120 @@ + + Projects + + Manage the projects that the user is in + + + Back to Users + + +
    $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 + + + +
    +
    + + +
    + +
    +
    + +
    + + + @php + $projects = \App\Models\Project::query() + ->where(function ($query) { + if (request()->has("q")) { + $query->where("name", "like", "%" . request("q") . "%"); + } + }) + ->take(5) + ->get(); + @endphp + + + + + + +
    + @foreach ($projects as $project) + + {{ $project->name }} + + @endforeach +
    +
    +
    +
    +
    +
    + + + Save + +
    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 0000000..eb4e1c5 --- /dev/null +++ b/resources/views/settings/users/partials/update-user-info.blade.php @@ -0,0 +1,99 @@ + + User Info + + You can update user's info here + +
    $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 +
    + + + @error("name") + + @enderror +
    + +
    + + + @error("email") + + @enderror +
    + +
    + + + @foreach (timezone_identifiers_list() as $timezone) + + @endforeach + + @error("timezone") + + @enderror +
    + +
    + + + + + + @error("role") + + @enderror +
    + +
    + + + @error("password") + + @enderror +
    +
    + + + Save + +
    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 0000000..b1b80e9 --- /dev/null +++ b/resources/views/settings/users/partials/update-user-password.blade.php @@ -0,0 +1,69 @@ + + + {{ __("Update Password") }} + + + + {{ __("Ensure your account is using a long, random password to stay secure.") }} + + +
    + @csrf + +
    + + + @error("current_password") + + @enderror +
    + +
    + + + @error("password") + + @enderror +
    + +
    + + + @error("password_confirmation") + + @enderror +
    +
    + + + {{ __("Save") }} + + +
    diff --git a/resources/views/settings/users/show.blade.php b/resources/views/settings/users/show.blade.php new file mode 100644 index 0000000..bb995ad --- /dev/null +++ b/resources/views/settings/users/show.blade.php @@ -0,0 +1,9 @@ + + Users + + + @include("settings.users.partials.update-projects") + + @include("settings.users.partials.update-user-info") + + 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 7bbb844..515d557 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 @@ @endforeach - + {{ __("Connect") }}
    diff --git a/routes/settings.php b/routes/settings.php index b548e61..7b70048 100644 --- a/routes/settings.php +++ b/routes/settings.php @@ -1,66 +1,62 @@ 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 7c05606..7326a48 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,8 @@ 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 18c8c98..28f7e5b 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 6b60d3e..aa37568 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 85b5b75..0f99abd 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 cca5f08..067263a 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 154d542..81b1fd6 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 a4577f9..2b4d687 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 0a64bb3..1fb04ef 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 45e0664..2979e5f 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, ]); }