Merge remote-tracking branch 'origin/next' into api-sensitive-data-scrubber

This commit is contained in:
Andras Bacsai 2026-06-15 13:29:25 +02:00
commit f5ecdfa4ce
339 changed files with 11186 additions and 2608 deletions

View file

@ -38,6 +38,43 @@ # Frontend
npm run build # production build npm run build # production build
``` ```
## Browser Tests (Pest Browser Plugin)
Uses `pestphp/pest-plugin-browser` with Laravel Dusk 8. New browser tests go in `tests/v4/Browser/`.
```bash
# Run all browser tests
php artisan test --compact tests/v4/Browser/
# Run a specific browser test file
php artisan test --compact tests/v4/Browser/LoginTest.php
# Run a specific test by name
php artisan test --compact --filter='can login with valid credentials'
```
### Writing Browser Tests
- Place new tests in `tests/v4/Browser/` — legacy Dusk tests in `tests/Browser/` should not be used as reference.
- Use `RefreshDatabase` and seed required data (at minimum `InstanceSettings::create(['id' => 0])`) in `beforeEach`.
- Key API: `visit()`, `fill(field, value)`, `click(text)`, `assertSee()`, `assertDontSee()`, `assertPathIs()`, `screenshot()`.
- Always call `screenshot()` at the end of each test for debugging.
- For authenticated tests, create a helper function that logs in via the UI:
```php
function loginAsRoot(): mixed
{
return visit('/login')
->fill('email', 'test@example.com')
->fill('password', 'password')
->click('Login');
}
```
- See `tests/v4/Browser/LoginTest.php`, `tests/v4/Browser/DashboardTest.php`, and `tests/v4/Browser/RegistrationTest.php` for conventions.
- Chrome driver runs on `localhost:4444`, app on `localhost:8000` (configured in `tests/DuskTestCase.php`).
- Legacy Dusk macros in `app/Providers/DuskServiceProvider.php` use the old `type()`/`press()` API — do not mix with Pest Browser Plugin's `fill()`/`click()` API.
## Architecture ## Architecture
### Backend Structure (app/) ### Backend Structure (app/)

View file

@ -18,6 +18,7 @@
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Str;
use Mail; use Mail;
use function Laravel\Prompts\confirm; use function Laravel\Prompts\confirm;

View file

@ -30,7 +30,6 @@
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class ApplicationsController extends Controller class ApplicationsController extends Controller
{ {
@ -64,6 +63,10 @@ private function removeSensitiveData($application)
$this->hideNestedServerSecrets($application); $this->hideNestedServerSecrets($application);
} }
if ($application->is_shown_once ?? false) {
$application->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($application); return serializeApiResponse($application);
} }
@ -1251,7 +1254,7 @@ private function create_application(Request $request, $type)
$application->isConfigurationChanged(true); $application->isConfigurationChanged(true);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -1490,7 +1493,7 @@ private function create_application(Request $request, $type)
$application->isConfigurationChanged(true); $application->isConfigurationChanged(true);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -1699,7 +1702,7 @@ private function create_application(Request $request, $type)
$application->isConfigurationChanged(true); $application->isConfigurationChanged(true);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -1745,7 +1748,7 @@ private function create_application(Request $request, $type)
], 422); ], 422);
} }
if (! $request->has('name')) { if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2); $request->offsetSet('name', 'dockerfile-'.new_public_id());
} }
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
@ -1819,7 +1822,7 @@ private function create_application(Request $request, $type)
$application->isConfigurationChanged(true); $application->isConfigurationChanged(true);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -1863,7 +1866,7 @@ private function create_application(Request $request, $type)
], 422); ], 422);
} }
if (! $request->has('name')) { if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2); $request->offsetSet('name', 'docker-image-'.new_public_id());
} }
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
@ -1938,7 +1941,7 @@ private function create_application(Request $request, $type)
$application->isConfigurationChanged(true); $application->isConfigurationChanged(true);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -2736,7 +2739,7 @@ public function update_by_uuid(Request $request)
]); ]);
if ($instantDeploy) { if ($instantDeploy) {
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -3643,7 +3646,7 @@ public function action_deploy(Request $request)
$this->authorize('deploy', $application); $this->authorize('deploy', $application);
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
@ -3841,7 +3844,7 @@ public function action_restart(Request $request)
$this->authorize('deploy', $application); $this->authorize('deploy', $application);
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,

View file

@ -182,6 +182,7 @@ public function show(Request $request)
if (is_null($token)) { if (is_null($token)) {
return response()->json(['message' => 'Cloud provider token not found.'], 404); return response()->json(['message' => 'Cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
return response()->json($this->removeSensitiveData($token)); return response()->json($this->removeSensitiveData($token));
} }
@ -248,6 +249,7 @@ public function store(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [CloudProviderToken::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
@ -399,6 +401,7 @@ public function update(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404); return response()->json(['message' => 'Cloud provider token not found.'], 404);
} }
$this->authorize('update', $token);
$token->update(array_intersect_key($body, array_flip($allowedFields))); $token->update(array_intersect_key($body, array_flip($allowedFields)));
@ -480,6 +483,7 @@ public function destroy(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404); return response()->json(['message' => 'Cloud provider token not found.'], 404);
} }
$this->authorize('delete', $token);
if ($token->hasServers()) { if ($token->hasServers()) {
return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400); return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400);
@ -550,9 +554,18 @@ public function validateToken(Request $request)
if (! $cloudToken) { if (! $cloudToken) {
return response()->json(['message' => 'Cloud provider token not found.'], 404); return response()->json(['message' => 'Cloud provider token not found.'], 404);
} }
$this->authorize('view', $cloudToken);
$validation = $this->validateProviderToken($cloudToken->provider, $cloudToken->token); $validation = $this->validateProviderToken($cloudToken->provider, $cloudToken->token);
auditLog('api.cloud_token.validated', [
'team_id' => $teamId,
'cloud_token_uuid' => $cloudToken->uuid,
'cloud_token_name' => $cloudToken->name,
'provider' => $cloudToken->provider,
'valid' => $validation['valid'],
]);
return response()->json([ return response()->json([
'valid' => $validation['valid'], 'valid' => $validation['valid'],
'message' => $validation['valid'] ? 'Token is valid.' : $validation['error'], 'message' => $validation['valid'] ? 'Token is valid.' : $validation['error'],

View file

@ -15,7 +15,6 @@
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
class DeployController extends Controller class DeployController extends Controller
{ {
@ -515,7 +514,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0, ?st
if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') { if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') {
return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null]; return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null];
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $resource, application: $resource,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,

View file

@ -183,6 +183,7 @@ public function create_github_app(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [GithubApp::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
return $return; return $return;
@ -564,6 +565,7 @@ public function update_github_app(Request $request, $github_app_id)
$githubApp = GithubApp::where('id', $github_app_id) $githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId) ->where('team_id', $teamId)
->firstOrFail(); ->firstOrFail();
$this->authorize('update', $githubApp);
// Define allowed fields for update // Define allowed fields for update
$allowedFields = [ $allowedFields = [
@ -737,6 +739,7 @@ public function delete_github_app($github_app_id)
$githubApp = GithubApp::where('id', $github_app_id) $githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId) ->where('team_id', $teamId)
->firstOrFail(); ->firstOrFail();
$this->authorize('delete', $githubApp);
// Check if the GitHub app is being used by any applications // Check if the GitHub app is being used by any applications
if ($githubApp->applications->isNotEmpty()) { if ($githubApp->applications->isNotEmpty()) {

View file

@ -116,6 +116,7 @@ public function locations(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404); return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
try { try {
$hetznerService = new HetznerService($token->token); $hetznerService = new HetznerService($token->token);
@ -237,6 +238,7 @@ public function serverTypes(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404); return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
try { try {
$hetznerService = new HetznerService($token->token); $hetznerService = new HetznerService($token->token);
@ -336,6 +338,7 @@ public function images(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404); return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
try { try {
$hetznerService = new HetznerService($token->token); $hetznerService = new HetznerService($token->token);
@ -445,6 +448,7 @@ public function sshKeys(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404); return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
try { try {
$hetznerService = new HetznerService($token->token); $hetznerService = new HetznerService($token->token);
@ -550,6 +554,7 @@ public function createServer(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [Server::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
@ -620,6 +625,7 @@ public function createServer(Request $request)
if (! $token) { if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404); return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
} }
$this->authorize('view', $token);
// Validate private key // Validate private key
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first(); $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();

View file

@ -97,6 +97,7 @@ public function project_by_uuid(Request $request)
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$this->authorize('view', $project);
$project->load(['environments']); $project->load(['environments']);
@ -233,6 +234,7 @@ public function create_project(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [Project::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
@ -385,6 +387,7 @@ public function update_project(Request $request)
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$this->authorize('update', $project);
$project->update($request->only($allowedFields)); $project->update($request->only($allowedFields));
@ -469,6 +472,7 @@ public function delete_project(Request $request)
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$this->authorize('delete', $project);
if (! $project->isEmpty()) { if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400); return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
} }
@ -652,6 +656,7 @@ public function create_environment(Request $request)
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$this->authorize('update', $project);
$existingEnvironment = $project->environments()->where('name', $request->name)->first(); $existingEnvironment = $project->environments()->where('name', $request->name)->first();
if ($existingEnvironment) { if ($existingEnvironment) {
@ -746,6 +751,7 @@ public function delete_environment(Request $request)
if (! $environment) { if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404); return response()->json(['message' => 'Environment not found.'], 404);
} }
$this->authorize('delete', $environment);
if (! $environment->isEmpty()) { if (! $environment->isEmpty()) {
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400); return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);

View file

@ -114,6 +114,7 @@ public function key_by_uuid(Request $request)
'message' => 'Private Key not found.', 'message' => 'Private Key not found.',
], 404); ], 404);
} }
$this->authorize('view', $key);
return response()->json($this->removeSensitiveData($key)); return response()->json($this->removeSensitiveData($key));
} }
@ -180,6 +181,7 @@ public function create_key(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [PrivateKey::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
return $return; return $return;
@ -342,6 +344,7 @@ public function update_key(Request $request)
'message' => 'Private Key not found.', 'message' => 'Private Key not found.',
], 404); ], 404);
} }
$this->authorize('update', $foundKey);
$foundKey->update($request->only($allowedFields)); $foundKey->update($request->only($allowedFields));
auditLog('api.private_key.updated', [ auditLog('api.private_key.updated', [
@ -425,6 +428,7 @@ public function delete_key(Request $request)
if (is_null($key)) { if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404); return response()->json(['message' => 'Private Key not found.'], 404);
} }
$this->authorize('delete', $key);
if ($key->isInUse()) { if ($key->isInUse()) {
return response()->json([ return response()->json([

View file

@ -97,12 +97,12 @@ public function push(Request $request)
if ($this->shouldDispatchUpdate($server, $data)) { if ($this->shouldDispatchUpdate($server, $data)) {
PushServerUpdateJob::dispatch($server, $data); PushServerUpdateJob::dispatch($server, $data);
}
auditLog('sentinel.metrics_pushed', [ auditLog('sentinel.metrics_pushed', [
'server_uuid' => $server->uuid, 'server_uuid' => $server->uuid,
'team_id' => $server->team_id, 'team_id' => $server->team_id,
]); ]);
}
return response()->json(['message' => 'ok'], 200); return response()->json(['message' => 'ok'], 200);
} }

View file

@ -156,6 +156,7 @@ public function server_by_uuid(Request $request)
if (is_null($server)) { if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
$this->authorize('view', $server);
if ($with_resources) { if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) { $server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [ $payload = [
@ -485,6 +486,7 @@ public function create_server(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$this->authorize('create', [ModelsServer::class]);
$return = validateIncomingRequest($request); $return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) { if ($return instanceof JsonResponse) {
@ -709,6 +711,7 @@ public function update_server(Request $request)
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
$this->authorize('update', $server);
if ($request->proxy_type) { if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) { $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower(); return str($proxyType->value)->lower();
@ -833,6 +836,7 @@ public function delete_server(Request $request)
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
$this->authorize('delete', $server);
$force = filter_var($request->query('force', false), FILTER_VALIDATE_BOOLEAN); $force = filter_var($request->query('force', false), FILTER_VALIDATE_BOOLEAN);
@ -932,6 +936,7 @@ public function validate_server(Request $request)
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
$this->authorize('update', $server);
ValidateServer::dispatch($server); ValidateServer::dispatch($server);
auditLog('api.server.validated', [ auditLog('api.server.validated', [

View file

@ -42,6 +42,10 @@ private function removeSensitiveData($service)
$this->exposeNestedServerSecrets($service); $this->exposeNestedServerSecrets($service);
} }
if ($service->is_shown_once ?? false) {
$service->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($service); return serializeApiResponse($service);
} }

View file

@ -110,6 +110,7 @@ public function team_by_id(Request $request)
if (is_null($team)) { if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404); return response()->json(['message' => 'Team not found.'], 404);
} }
$this->authorize('view', $team);
$team = $this->removeSensitiveData($team); $team = $this->removeSensitiveData($team);
return response()->json( return response()->json(
@ -168,6 +169,7 @@ public function members_by_id(Request $request)
if (is_null($team)) { if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404); return response()->json(['message' => 'Team not found.'], 404);
} }
$this->authorize('view', $team);
$members = $team->members; $members = $team->members;
$members->makeHidden([ $members->makeHidden([
'pivot', 'pivot',

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
@ -11,6 +12,8 @@
class UploadController extends BaseController class UploadController extends BaseController
{ {
use AuthorizesRequests;
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
private const ALLOWED_EXTENSIONS = [ private const ALLOWED_EXTENSIONS = [
@ -40,6 +43,8 @@ public function upload(Request $request)
return response()->json(['error' => 'You do not have permission for this database'], 500); return response()->json(['error' => 'You do not have permission for this database'], 500);
} }
$this->authorize('uploadBackup', $resource);
$chunk = $request->file('file'); $chunk = $request->file('file');
$originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null; $originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null;
if (blank($originalName) || ! self::hasAllowedExtension($originalName)) { if (blank($originalName) || ! self::hasAllowedExtension($originalName)) {

View file

@ -10,7 +10,6 @@
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Bitbucket extends Controller class Bitbucket extends Controller
{ {
@ -141,7 +140,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@ -192,7 +191,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {

View file

@ -11,7 +11,6 @@
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Gitea extends Controller class Gitea extends Controller
{ {
@ -127,7 +126,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@ -194,7 +193,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {

View file

@ -17,7 +17,6 @@
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Github extends Controller class Github extends Controller
{ {
@ -144,7 +143,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@ -362,7 +361,7 @@ public function normal(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,

View file

@ -11,7 +11,6 @@
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Gitlab extends Controller class Gitlab extends Controller
{ {
@ -168,7 +167,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $application, application: $application,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@ -236,7 +235,7 @@ public function manual(Request $request)
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {

View file

@ -7,9 +7,34 @@
class ApiAbility extends CheckForAnyAbility class ApiAbility extends CheckForAnyAbility
{ {
/**
* Permissions that only admins/owners may use.
*/
private const MEMBER_DISALLOWED_ABILITIES = [
'root',
'write',
'write:sensitive',
'deploy',
'read:sensitive',
];
public function handle($request, $next, ...$abilities) public function handle($request, $next, ...$abilities)
{ {
try { try {
$token = $request->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
if ($teamId !== null && ! $request->user()->isAdminOfTeam((int) $teamId)) {
$tokenAbilities = $token->abilities ?? [];
$disallowed = array_intersect($tokenAbilities, self::MEMBER_DISALLOWED_ABILITIES);
if (! empty($disallowed)) {
return response()->json([
'message' => 'This API token has permissions ('.implode(', ', $disallowed).') that exceed your current role as a team member. Members are restricted to read-only API access. Please revoke this token and create a new one with only read permissions.',
], 403);
}
}
if ($request->user()->tokenCan('root')) { if ($request->user()->tokenCan('root')) {
return $next($request); return $next($request);
} }

View file

@ -10,10 +10,13 @@ class ApiSensitiveData
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
$token = $request->user()->currentAccessToken(); $token = $request->user()->currentAccessToken();
$hasTokenPermission = $token->can('root') || $token->can('read:sensitive');
$teamId = (int) data_get($token, 'team_id');
$isAdmin = $teamId ? $request->user()->isAdminOfTeam($teamId) : false;
// Allow access to sensitive data if token has root or read:sensitive permission // Allow access to sensitive data only if token has permission AND user is admin/owner
$request->attributes->add([ $request->attributes->add([
'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'), 'can_read_sensitive' => $hasTokenPermission && $isAdmin,
]); ]);
return $next($request); return $next($request);

View file

@ -37,7 +37,6 @@
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
use Visus\Cuid2\Cuid2;
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{ {
@ -2207,7 +2206,7 @@ private function deploy_to_additional_destinations()
continue; continue;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
queue_application_deployment( queue_application_deployment(
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
application: $this->application, application: $this->application,

View file

@ -27,7 +27,6 @@
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable; use Throwable;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
@ -309,7 +308,7 @@ public function handle(): void
// Generate unique UUID for each database backup execution // Generate unique UUID for each database backup execution
$attempts = 0; $attempts = 0;
do { do {
$this->backup_log_uuid = (string) new Cuid2; $this->backup_log_uuid = new_public_id();
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists(); $exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
$attempts++; $attempts++;
if ($attempts >= 3 && $exists) { if ($attempts >= 3 && $exists) {

View file

@ -15,7 +15,6 @@
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Throwable; use Throwable;
use Visus\Cuid2\Cuid2;
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
{ {
@ -166,7 +165,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
} }
// Queue the deployment // Queue the deployment
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
queue_application_deployment( queue_application_deployment(
application: $application, application: $application,
pull_request_id: $this->pullRequestId, pull_request_id: $this->pullRequestId,

View file

@ -54,6 +54,9 @@ public function submitSearch()
public function getSubscribers() public function getSubscribers()
{ {
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count(); $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count(); $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
} }

View file

@ -9,13 +9,15 @@
use App\Models\Team; use App\Models\Team;
use App\Services\ConfigurationRepository; use App\Services\ConfigurationRepository;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Url; use Livewire\Attributes\Url;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Index extends Component class Index extends Component
{ {
use AuthorizesRequests;
protected $listeners = [ protected $listeners = [
'refreshBoardingIndex' => 'validateServer', 'refreshBoardingIndex' => 'validateServer',
'prerequisitesInstalled' => 'handlePrerequisitesInstalled', 'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
@ -174,6 +176,9 @@ public function restartBoarding()
public function skipBoarding() public function skipBoarding()
{ {
if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
}
Team::find(currentTeam()->id)->update([ Team::find(currentTeam()->id)->update([
'show_boarding' => false, 'show_boarding' => false,
]); ]);
@ -276,6 +281,7 @@ public function savePrivateKey()
]); ]);
try { try {
$this->authorize('create', PrivateKey::class);
$privateKey = PrivateKey::createAndStore([ $privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName, 'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription, 'description' => $this->privateKeyDescription,
@ -294,6 +300,12 @@ public function saveServer()
{ {
$this->validate(); $this->validate();
try {
$this->authorize('create', Server::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->privateKey = formatPrivateKey($this->privateKey); $this->privateKey = formatPrivateKey($this->privateKey);
$foundServer = Server::whereIp($this->remoteServerHost)->first(); $foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) { if ($foundServer) {
@ -457,7 +469,7 @@ public function createNewProject()
$this->createdProject = Project::create([ $this->createdProject = Project::create([
'name' => 'My first project', 'name' => 'My first project',
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
$this->currentState = 'create-resource'; $this->currentState = 'create-resource';
} }

View file

@ -9,7 +9,6 @@
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Docker extends Component class Docker extends Component
{ {
@ -35,7 +34,7 @@ class Docker extends Component
public function mount(?string $server_id = null): void public function mount(?string $server_id = null): void
{ {
$this->network = (string) new Cuid2; $this->network = new_public_id();
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
if (filled($server_id)) { if (filled($server_id)) {
@ -68,7 +67,7 @@ public function updatedServerId(): void
public function generateName(): void public function generateName(): void
{ {
$name = data_get($this->selectedServer, 'name', new Cuid2); $name = data_get($this->selectedServer, 'name', new_public_id());
$this->name = str("{$name}-{$this->network}")->kebab(); $this->name = str("{$name}-{$this->network}")->kebab();
} }

View file

@ -3,6 +3,7 @@
namespace App\Livewire\Destination; namespace App\Livewire\Destination;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
@ -31,8 +32,12 @@ public function mount(string $destination_uuid)
if (! $destination) { if (! $destination) {
return redirect()->route('destination.index'); return redirect()->route('destination.index');
} }
$this->authorize('view', $destination);
$this->destination = $destination; $this->destination = $destination;
$this->syncData(); $this->syncData();
} catch (AuthorizationException) {
abort(403);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View file

@ -4,7 +4,6 @@
// use Livewire\Component; // use Livewire\Component;
use Illuminate\View\Component; use Illuminate\View\Component;
use Visus\Cuid2\Cuid2;
class MonacoEditor extends Component class MonacoEditor extends Component
{ {
@ -40,7 +39,7 @@ public function __construct(
public function render() public function render()
{ {
if (is_null($this->id)) { if (is_null($this->id)) {
$this->id = new Cuid2; $this->id = new_public_id();
} }
if (is_null($this->name)) { if (is_null($this->name)) {

View file

@ -2,12 +2,16 @@
namespace App\Livewire; namespace App\Livewire;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class NavbarDeleteTeam extends Component class NavbarDeleteTeam extends Component
{ {
use AuthorizesRequests;
public $team; public $team;
public function mount() public function mount()
@ -17,12 +21,13 @@ public function mount()
public function delete($password, $selectedActions = []) public function delete($password, $selectedActions = [])
{ {
try {
if (! verifyPasswordConfirmation($password, $this)) { if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.'; return 'The provided password is incorrect.';
} }
$currentTeam = currentTeam(); $currentTeam = currentTeam();
$currentTeam->delete(); $this->authorize('delete', $currentTeam);
$currentTeam->members->each(function ($user) use ($currentTeam) { $currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === Auth::id()) { if ($user->id === Auth::id()) {
@ -35,9 +40,16 @@ public function delete($password, $selectedActions = [])
} }
}); });
refreshSession(); Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id);
$currentTeam->delete();
return redirectRoute($this, 'team.index'); $newTeam = Auth::user()->teams()->first();
refreshSession($newTeam);
return redirect()->route('team.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -110,7 +110,9 @@ public function syncData(bool $toModel = false)
refreshSession(); refreshSession();
} else { } else {
$this->discordEnabled = $this->settings->discord_enabled; $this->discordEnabled = $this->settings->discord_enabled;
$this->discordWebhookUrl = $this->settings->discord_webhook_url; $this->discordWebhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->discord_webhook_url
: null;
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications; $this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications; $this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;

View file

@ -113,8 +113,13 @@ public function syncData(bool $toModel = false)
refreshSession(); refreshSession();
} else { } else {
$this->pushoverEnabled = $this->settings->pushover_enabled; $this->pushoverEnabled = $this->settings->pushover_enabled;
if (auth()->user()->can('update', $this->settings)) {
$this->pushoverUserKey = $this->settings->pushover_user_key; $this->pushoverUserKey = $this->settings->pushover_user_key;
$this->pushoverApiToken = $this->settings->pushover_api_token; $this->pushoverApiToken = $this->settings->pushover_api_token;
} else {
$this->pushoverUserKey = null;
$this->pushoverApiToken = null;
}
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications; $this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications; $this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;

View file

@ -110,7 +110,9 @@ public function syncData(bool $toModel = false)
refreshSession(); refreshSession();
} else { } else {
$this->slackEnabled = $this->settings->slack_enabled; $this->slackEnabled = $this->settings->slack_enabled;
$this->slackWebhookUrl = $this->settings->slack_webhook_url; $this->slackWebhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->slack_webhook_url
: null;
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications; $this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications; $this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;

View file

@ -169,8 +169,13 @@ public function syncData(bool $toModel = false)
$this->settings->save(); $this->settings->save();
} else { } else {
$this->telegramEnabled = $this->settings->telegram_enabled; $this->telegramEnabled = $this->settings->telegram_enabled;
if (auth()->user()->can('update', $this->settings)) {
$this->telegramToken = $this->settings->telegram_token; $this->telegramToken = $this->settings->telegram_token;
$this->telegramChatId = $this->settings->telegram_chat_id; $this->telegramChatId = $this->settings->telegram_chat_id;
} else {
$this->telegramToken = null;
$this->telegramChatId = null;
}
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications; $this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications; $this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;

View file

@ -105,7 +105,9 @@ public function syncData(bool $toModel = false)
refreshSession(); refreshSession();
} else { } else {
$this->webhookEnabled = $this->settings->webhook_enabled; $this->webhookEnabled = $this->settings->webhook_enabled;
$this->webhookUrl = $this->settings->webhook_url; $this->webhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->webhook_url
: null;
$this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications; $this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications;
$this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications; $this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications;

View file

@ -4,11 +4,13 @@
use App\Models\Project; use App\Models\Project;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class AddEmpty extends Component class AddEmpty extends Component
{ {
use AuthorizesRequests;
public string $name; public string $name;
public string $description = ''; public string $description = '';
@ -29,12 +31,13 @@ protected function messages(): array
public function submit() public function submit()
{ {
try { try {
$this->authorize('create', Project::class);
$this->validate(); $this->validate();
$project = Project::create([ $project = Project::create([
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->description,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
$productionEnvironment = $project->environments()->where('name', 'production')->first(); $productionEnvironment = $project->environments()->where('name', 'production')->first();

View file

@ -59,7 +59,9 @@ public function mount()
$this->application_deployment_queue = $application_deployment_queue; $this->application_deployment_queue = $application_deployment_queue;
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus(); $this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->deployment_uuid = $deploymentUuid; $this->deployment_uuid = $deploymentUuid;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = auth()->user()->isMember()
? false
: $this->application->settings->is_debug_enabled;
$this->isKeepAliveOn(); $this->isKeepAliveOn();
} }
@ -110,6 +112,8 @@ public function getLogLinesProperty()
public function downloadAllLogs(): string public function downloadAllLogs(): string
{ {
$this->authorize('update', $this->application);
$logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true) $logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true)
->map(function ($line) { ->map(function ($line) {
$prefix = ''; $prefix = '';

View file

@ -6,11 +6,14 @@
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Livewire\Component; use Livewire\Component;
class DeploymentNavbar extends Component class DeploymentNavbar extends Component
{ {
use AuthorizesRequests;
public ApplicationDeploymentQueue $application_deployment_queue; public ApplicationDeploymentQueue $application_deployment_queue;
public Application $application; public Application $application;
@ -25,7 +28,9 @@ public function mount()
{ {
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id); $this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
$this->server = $this->application->destination->server; $this->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = auth()->user()->isMember()
? false
: $this->application->settings->is_debug_enabled;
} }
public function deploymentFinished() public function deploymentFinished()
@ -35,15 +40,21 @@ public function deploymentFinished()
public function show_debug() public function show_debug()
{ {
try {
$this->authorize('update', $this->application);
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled; $this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
$this->application->settings->save(); $this->application->settings->save();
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue'); $this->dispatch('refreshQueue');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function force_start() public function force_start()
{ {
try { try {
$this->authorize('deploy', $this->application);
force_start_deployment($this->application_deployment_queue); force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@ -58,10 +69,15 @@ public function copyLogsToClipboard(): string
return ''; return '';
} }
$isMember = auth()->user()->isMember();
$markdown = "# Deployment Logs\n\n"; $markdown = "# Deployment Logs\n\n";
$markdown .= "```\n"; $markdown .= "```\n";
foreach ($logs as $log) { foreach ($logs as $log) {
if ($isMember && ! empty($log['hidden'])) {
continue;
}
if (isset($log['output'])) { if (isset($log['output'])) {
$markdown .= $log['output']."\n"; $markdown .= $log['output']."\n";
} }
@ -74,6 +90,11 @@ public function copyLogsToClipboard(): string
public function cancel() public function cancel()
{ {
try {
$this->authorize('deploy', $this->application);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$deployment_uuid = $this->application_deployment_queue->deployment_uuid; $deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$kill_command = "docker rm -f {$deployment_uuid}"; $kill_command = "docker rm -f {$deployment_uuid}";
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id; $build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;

View file

@ -13,7 +13,6 @@
use Livewire\Component; use Livewire\Component;
use Livewire\Features\SupportEvents\Event; use Livewire\Features\SupportEvents\Event;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component class General extends Component
{ {
@ -549,7 +548,7 @@ public function generateDomain(string $serviceName)
try { try {
$this->authorize('update', $this->application); $this->authorize('update', $this->application);
$uuid = new Cuid2; $uuid = new_public_id();
$domain = generateUrl(server: $this->application->destination->server, random: $uuid); $domain = generateUrl(server: $this->application->destination->server, random: $uuid);
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString(); $sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain; $this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;

View file

@ -7,7 +7,6 @@
use App\Models\Application; use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Heading extends Component class Heading extends Component
{ {
@ -65,13 +64,18 @@ public function manualCheckStatus()
public function force_deploy_without_cache() public function force_deploy_without_cache()
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
$this->deploy(force_rebuild: true); $this->deploy(force_rebuild: true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
@ -117,24 +121,32 @@ public function deploy(bool $force_rebuild = false)
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'], 'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false); ], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
protected function setDeploymentUuid() protected function setDeploymentUuid()
{ {
$this->deploymentUuid = new Cuid2; $this->deploymentUuid = new_public_id();
$this->parameters['deployment_uuid'] = $this->deploymentUuid; $this->parameters['deployment_uuid'] = $this->deploymentUuid;
} }
public function stop() public function stop()
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.'); $this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup); StopApplication::dispatch($this->application, false, $this->docker_cleanup);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function restart() public function restart()
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
@ -166,6 +178,9 @@ public function restart()
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'], 'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false); ], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -9,7 +9,6 @@
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Previews extends Component class Previews extends Component
{ {
@ -234,6 +233,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null,
public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null) public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null)
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
$dockerRegistryImageTag = null; $dockerRegistryImageTag = null;
@ -244,21 +244,27 @@ public function force_deploy_without_cache(int $pull_request_id, ?string $pull_r
} }
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag); $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null) public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
{ {
try {
$this->authorize('deploy', $this->application); $this->authorize('deploy', $this->application);
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag); $this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag); $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null) public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null)
{ {
$this->authorize('deploy', $this->application);
try { try {
$this->authorize('deploy', $this->application);
$this->setDeploymentUuid(); $this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) { if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
@ -305,7 +311,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu
protected function setDeploymentUuid() protected function setDeploymentUuid()
{ {
$this->deployment_uuid = new Cuid2; $this->deployment_uuid = new_public_id();
$this->parameters['deployment_uuid'] = $this->deployment_uuid; $this->parameters['deployment_uuid'] = $this->deployment_uuid;
} }
@ -350,9 +356,8 @@ private function stopContainers(array $containers, $server)
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
$this->authorize('deploy', $this->application);
try { try {
$this->authorize('deploy', $this->application);
$server = $this->application->destination->server; $server = $this->application->destination->server;
if ($this->application->destination->server->isSwarm()) { if ($this->application->destination->server->isSwarm()) {

View file

@ -6,7 +6,6 @@
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class PreviewsCompose extends Component class PreviewsCompose extends Component
{ {
@ -64,7 +63,7 @@ public function generate()
if (empty($domain_string)) { if (empty($domain_string)) {
$server = $this->preview->application->destination->server; $server = $this->preview->application->destination->server;
$template = $this->preview->application->preview_url_template; $template = $this->preview->application->preview_url_template;
$random = new Cuid2; $random = new_public_id();
// Generate a unique domain like main app services do // Generate a unique domain like main app services do
$generated_fqdn = generateUrl(server: $server, random: $random); $generated_fqdn = generateUrl(server: $server, random: $random);
@ -79,7 +78,7 @@ public function generate()
$domain_list = explode(',', $domain_string); $domain_list = explode(',', $domain_string);
$preview_fqdns = []; $preview_fqdns = [];
$template = $this->preview->application->preview_url_template; $template = $this->preview->application->preview_url_template;
$random = new Cuid2; $random = new_public_id();
foreach ($domain_list as $single_domain) { foreach ($domain_list as $single_domain) {
$single_domain = trim($single_domain); $single_domain = trim($single_domain);

View file

@ -6,7 +6,6 @@
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Rollback extends Component class Rollback extends Component
{ {
@ -52,7 +51,7 @@ public function rollbackImage($commit)
$commit = validateGitRef($commit, 'rollback commit'); $commit = validateGitRef($commit, 'rollback commit');
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$result = queue_application_deployment( $result = queue_application_deployment(
application: $this->application, application: $this->application,

View file

@ -3,11 +3,14 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Swarm extends Component class Swarm extends Component
{ {
use AuthorizesRequests;
public Application $application; public Application $application;
#[Validate('required')] #[Validate('required')]
@ -51,6 +54,7 @@ public function syncData(bool $toModel = false)
public function instantSave() public function instantSave()
{ {
try { try {
$this->authorize('update', $this->application);
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.'); $this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -61,6 +65,7 @@ public function instantSave()
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->application);
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.'); $this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View file

@ -11,11 +11,13 @@
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneMe extends Component class CloneMe extends Component
{ {
use AuthorizesRequests;
public string $project_uuid; public string $project_uuid;
public string $environment_uuid; public string $environment_uuid;
@ -61,7 +63,7 @@ public function mount($project_uuid)
->servers() ->servers()
->get() ->get()
->reject(fn ($server) => $server->isBuildServer()); ->reject(fn ($server) => $server->isBuildServer());
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); $this->newName = str($this->project->name.'-clone-'.new_public_id())->slug();
} }
public function toggleVolumeCloning(bool $value) public function toggleVolumeCloning(bool $value)
@ -91,6 +93,7 @@ public function selectServer($server_id, $destination_id)
public function clone(string $type) public function clone(string $type)
{ {
try { try {
$this->authorize('create', Project::class);
$this->validate([ $this->validate([
'selectedDestination' => 'required', 'selectedDestination' => 'required',
'newName' => ValidationPatterns::nameRules(), 'newName' => ValidationPatterns::nameRules(),
@ -108,7 +111,7 @@ public function clone(string $type)
if ($this->environment->name !== 'production') { if ($this->environment->name !== 'production') {
$project->environments()->create([ $project->environments()->create([
'name' => $this->environment->name, 'name' => $this->environment->name,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
} }
$environment = $project->environments->where('name', $this->environment->name)->first(); $environment = $project->environments->where('name', $this->environment->name)->first();
@ -120,7 +123,7 @@ public function clone(string $type)
$project = $this->project; $project = $this->project;
$environment = $this->project->environments()->create([ $environment = $this->project->environments()->create([
'name' => $this->newName, 'name' => $this->newName,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
} }
$applications = $this->environment->applications; $applications = $this->environment->applications;
@ -134,7 +137,7 @@ public function clone(string $type)
} }
foreach ($databases as $database) { foreach ($databases as $database) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$newDatabase = $database->replicate([ $newDatabase = $database->replicate([
'id', 'id',
'created_at', 'created_at',
@ -225,7 +228,7 @@ public function clone(string $type)
$scheduledBackups = $database->scheduledBackups()->get(); $scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) { foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$newBackup = $backup->replicate([ $newBackup = $backup->replicate([
'id', 'id',
'created_at', 'created_at',
@ -254,7 +257,7 @@ public function clone(string $type)
} }
foreach ($services as $service) { foreach ($services as $service) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$newService = $service->replicate([ $newService = $service->replicate([
'id', 'id',
'created_at', 'created_at',
@ -278,7 +281,7 @@ public function clone(string $type)
'created_at', 'created_at',
'updated_at', 'updated_at',
])->fill([ ])->fill([
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
'service_id' => $newService->id, 'service_id' => $newService->id,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
@ -409,7 +412,7 @@ public function clone(string $type)
$scheduledBackups = $database->scheduledBackups()->get(); $scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) { foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$newBackup = $backup->replicate([ $newBackup = $backup->replicate([
'id', 'id',
'created_at', 'created_at',

View file

@ -2,6 +2,7 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Jobs\DatabaseBackupJob;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Exception; use Exception;
@ -190,6 +191,18 @@ public function delete($password, $selectedActions = [])
} }
} }
public function backupNow()
{
try {
$this->authorize('manageBackups', $this->backup->database);
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {

View file

@ -3,12 +3,16 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
use AuthorizesRequests;
public ?ScheduledDatabaseBackup $backup = null; public ?ScheduledDatabaseBackup $backup = null;
public $database; public $database;
@ -44,15 +48,22 @@ public function getListeners()
public function cleanupFailed() public function cleanupFailed()
{ {
try {
$this->authorize('manageBackups', $this->database);
if ($this->backup) { if ($this->backup) {
$this->backup->executions()->where('status', 'failed')->delete(); $this->backup->executions()->where('status', 'failed')->delete();
$this->refreshBackupExecutions(); $this->refreshBackupExecutions();
$this->dispatch('success', 'Failed backups cleaned up.'); $this->dispatch('success', 'Failed backups cleaned up.');
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function cleanupDeleted() public function cleanupDeleted()
{ {
try {
$this->authorize('manageBackups', $this->database);
if ($this->backup) { if ($this->backup) {
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count(); $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
if ($deletedCount > 0) { if ($deletedCount > 0) {
@ -63,10 +74,19 @@ public function cleanupDeleted()
$this->dispatch('info', 'No backup entries found that are deleted from local storage.'); $this->dispatch('info', 'No backup entries found that are deleted from local storage.');
} }
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function deleteBackup($executionId, $password, $selectedActions = []) public function deleteBackup($executionId, $password, $selectedActions = [])
{ {
try {
$this->authorize('manageBackups', $this->database);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if (! verifyPasswordConfirmation($password, $this)) { if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.'; return 'The provided password is incorrect.';
} }
@ -78,7 +98,7 @@ public function deleteBackup($executionId, $password, $selectedActions = [])
return; return;
} }
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class $server = $execution->scheduledDatabaseBackup->database->getMorphClass() === ServiceDatabase::class
? $execution->scheduledDatabaseBackup->database->service->destination->server ? $execution->scheduledDatabaseBackup->database->service->destination->server
: $execution->scheduledDatabaseBackup->database->destination->server; : $execution->scheduledDatabaseBackup->database->destination->server;
@ -185,7 +205,7 @@ public function server()
if ($this->database) { if ($this->database) {
$server = null; $server = null;
if ($this->database instanceof \App\Models\ServiceDatabase) { if ($this->database instanceof ServiceDatabase) {
$server = $this->database->service->destination->server; $server = $this->database->service->destination->server;
} elseif ($this->database->destination && $this->database->destination->server) { } elseif ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server; $server = $this->database->destination->server;

View file

@ -14,9 +14,13 @@ class BackupNow extends Component
public function backupNow() public function backupNow()
{ {
try {
$this->authorize('manageBackups', $this->backup->database); $this->authorize('manageBackups', $this->backup->database);
DatabaseBackupJob::dispatch($this->backup); DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View file

@ -42,6 +42,8 @@ class General extends Component
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array public function getListeners(): array
{ {
$user = Auth::user(); $user = Auth::user();
@ -72,6 +74,11 @@ public function mount()
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->clickhouseAdminPassword = '';
}
} }
protected function rules(): array protected function rules(): array

View file

@ -40,6 +40,8 @@ class General extends Component
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array public function getListeners(): array
{ {
$user = Auth::user(); $user = Auth::user();
@ -70,6 +72,11 @@ public function mount()
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->dragonflyPassword = '';
}
} }
protected function rules(): array protected function rules(): array

View file

@ -90,18 +90,28 @@ public function stop()
public function restart() public function restart()
{ {
try {
$this->authorize('manage', $this->database); $this->authorize('manage', $this->database);
$activity = RestartDatabase::run($this->database); $activity = RestartDatabase::run($this->database);
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function start() public function start()
{ {
try {
$this->authorize('manage', $this->database); $this->authorize('manage', $this->database);
$activity = StartDatabase::run($this->database); $activity = StartDatabase::run($this->database);
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -2,13 +2,20 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\StandalonePostgresql;
use Exception; use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class InitScript extends Component class InitScript extends Component
{ {
use AuthorizesRequests;
#[Locked]
public StandalonePostgresql $database;
#[Locked] #[Locked]
public array $script; public array $script;
@ -35,6 +42,7 @@ public function mount()
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->database);
$this->validate(); $this->validate();
$this->script['index'] = $this->index; $this->script['index'] = $this->index;
$this->script['content'] = $this->content; $this->script['content'] = $this->content;
@ -48,6 +56,7 @@ public function submit()
public function delete() public function delete()
{ {
try { try {
$this->authorize('update', $this->database);
$this->dispatch('delete_init_script', $this->script); $this->dispatch('delete_init_script', $this->script);
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View file

@ -42,6 +42,8 @@ class General extends Component
public bool $isLogDrainEnabled = false; public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array public function getListeners(): array
{ {
$user = Auth::user(); $user = Auth::user();
@ -72,6 +74,11 @@ public function mount()
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->keydbPassword = '';
}
} }
protected function rules(): array protected function rules(): array

View file

@ -47,6 +47,8 @@ class General extends Component
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array protected function rules(): array
{ {
return [ return [
@ -126,6 +128,12 @@ public function mount()
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mariadbRootPassword = '';
$this->mariadbPassword = '';
}
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)

View file

@ -45,6 +45,8 @@ class General extends Component
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array protected function rules(): array
{ {
return [ return [
@ -119,6 +121,11 @@ public function mount()
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mongoInitdbRootPassword = '';
}
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)

View file

@ -47,6 +47,8 @@ class General extends Component
public ?string $customDockerRunOptions = null; public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array protected function rules(): array
{ {
return [ return [
@ -126,6 +128,12 @@ public function mount()
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mysqlRootPassword = '';
$this->mysqlPassword = '';
}
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)

View file

@ -55,6 +55,8 @@ class General extends Component
public string $new_content; public string $new_content;
public bool $isPasswordHiddenForMember = false;
protected $listeners = [ protected $listeners = [
'save_init_script', 'save_init_script',
'delete_init_script', 'delete_init_script',
@ -140,6 +142,11 @@ public function mount()
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->postgresPassword = '';
}
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)

View file

@ -45,6 +45,8 @@ class General extends Component
public string $redisVersion; public string $redisVersion;
public bool $isPasswordHiddenForMember = false;
protected $listeners = [ protected $listeners = [
'envsUpdated' => 'refresh', 'envsUpdated' => 'refresh',
]; ];
@ -118,6 +120,11 @@ public function mount()
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->redisPassword = '';
}
} }
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)

View file

@ -3,6 +3,7 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
@ -34,7 +35,7 @@ public function mount(): void
$this->setSelectedBackup($this->selectedBackupId, true); $this->setSelectedBackup($this->selectedBackupId, true);
} }
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { if ($this->database->getMorphClass() === ServiceDatabase::class) {
$this->type = 'service-database'; $this->type = 'service-database';
} else { } else {
$this->type = 'database'; $this->type = 'database';
@ -56,22 +57,30 @@ public function setSelectedBackup($backupId, $force = false)
public function setCustomType() public function setCustomType()
{ {
try {
$this->authorize('update', $this->database); $this->authorize('update', $this->database);
$this->database->custom_type = $this->custom_type; $this->database->custom_type = $this->custom_type;
$this->database->save(); $this->database->save();
$this->dispatch('success', 'Database type set.'); $this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups(); $this->refreshScheduledBackups();
} catch (\Throwable $e) {
handleError($e, $this);
}
} }
public function delete($scheduled_backup_id): void public function delete($scheduled_backup_id): void
{ {
$backup = $this->database->scheduledBackups->find($scheduled_backup_id); try {
$this->authorize('manageBackups', $this->database); $this->authorize('manageBackups', $this->database);
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
$backup->delete(); $backup->delete();
$this->dispatch('success', 'Scheduled backup deleted.'); $this->dispatch('success', 'Scheduled backup deleted.');
$this->refreshScheduledBackups(); $this->refreshScheduledBackups();
} catch (\Throwable $e) {
handleError($e, $this);
}
} }
public function refreshScheduledBackups(?int $id = null): void public function refreshScheduledBackups(?int $id = null): void

View file

@ -28,6 +28,7 @@ public function mount()
public function delete() public function delete()
{ {
try {
$this->validate([ $this->validate([
'environment_id' => 'required|int', 'environment_id' => 'required|int',
]); ]);
@ -41,5 +42,8 @@ public function delete()
} }
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first."); return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View file

@ -26,6 +26,7 @@ public function mount()
public function delete() public function delete()
{ {
try {
$this->validate([ $this->validate([
'project_id' => 'required|int', 'project_id' => 'required|int',
]); ]);
@ -39,5 +40,8 @@ public function delete()
} }
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first."); return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View file

@ -4,10 +4,13 @@
use App\Models\Project; use App\Models\Project;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Edit extends Component class Edit extends Component
{ {
use AuthorizesRequests;
public Project $project; public Project $project;
public string $name; public string $name;
@ -54,6 +57,7 @@ public function syncData(bool $toModel = false)
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->project);
$this->syncData(true); $this->syncData(true);
$this->dispatch('success', 'Project updated.'); $this->dispatch('success', 'Project updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View file

@ -5,11 +5,14 @@
use App\Models\Application; use App\Models\Application;
use App\Models\Project; use App\Models\Project;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Component; use Livewire\Component;
class EnvironmentEdit extends Component class EnvironmentEdit extends Component
{ {
use AuthorizesRequests;
public Project $project; public Project $project;
public Application $application; public Application $application;
@ -62,6 +65,7 @@ public function syncData(bool $toModel = false)
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->environment);
$this->syncData(true); $this->syncData(true);
redirectRoute($this, 'project.environment.edit', [ redirectRoute($this, 'project.environment.edit', [
'environment_uuid' => $this->environment->uuid, 'environment_uuid' => $this->environment->uuid,

View file

@ -7,7 +7,6 @@
use App\Services\DockerImageParser; use App\Services\DockerImageParser;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class DockerImage extends Component class DockerImage extends Component
{ {
@ -130,7 +129,7 @@ public function submit()
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::create([ $application = Application::create([
'name' => 'docker-image-'.new Cuid2, 'name' => 'docker-image-'.new_public_id(),
'repository_project_id' => 0, 'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main', 'git_branch' => 'main',

View file

@ -4,7 +4,6 @@
use App\Models\Project; use App\Models\Project;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class EmptyProject extends Component class EmptyProject extends Component
{ {
@ -13,7 +12,7 @@ public function createEmptyProject()
$project = Project::create([ $project = Project::create([
'name' => generate_random_name(), 'name' => generate_random_name(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]); return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);

View file

@ -6,7 +6,6 @@
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\Project; use App\Models\Project;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class SimpleDockerfile extends Component class SimpleDockerfile extends Component
{ {
@ -48,7 +47,7 @@ public function submit()
$port = 80; $port = 80;
} }
$application = Application::create([ $application = Application::create([
'name' => 'dockerfile-'.new Cuid2, 'name' => 'dockerfile-'.new_public_id(),
'repository_project_id' => 0, 'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main', 'git_branch' => 'main',

View file

@ -3,10 +3,13 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\Service; use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class EditCompose extends Component class EditCompose extends Component
{ {
use AuthorizesRequests;
public Service $service; public Service $service;
public $serviceId; public $serviceId;
@ -72,19 +75,29 @@ public function validateCompose()
public function saveEditedCompose() public function saveEditedCompose()
{ {
try {
$this->authorize('update', $this->service);
$this->dispatch('info', 'Saving new docker compose...'); $this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->dockerComposeRaw); $this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->dispatch('refreshStorages'); $this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function instantSave() public function instantSave()
{ {
try {
$this->authorize('update', $this->service);
$this->validate([ $this->validate([
'isContainerLabelEscapeEnabled' => 'required', 'isContainerLabelEscapeEnabled' => 'required',
]); ]);
$this->syncData(true); $this->syncData(true);
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]); $this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
$this->dispatch('success', 'Service updated successfully'); $this->dispatch('success', 'Service updated successfully');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -110,8 +110,7 @@ public function convertToDirectory()
public function loadStorageOnServer() public function loadStorageOnServer()
{ {
try { try {
// Loading content is a read operation, so we use 'view' permission $this->authorize('update', $this->resource);
$this->authorize('view', $this->resource);
$this->fileStorage->loadStorageOnServer(); $this->fileStorage->loadStorageOnServer();
$this->syncData(); $this->syncData();

View file

@ -110,17 +110,20 @@ public function checkDeployments()
public function start() public function start()
{ {
try {
$this->authorizeService('deploy'); $this->authorizeService('deploy');
$activity = StartService::run($this->service, pullLatestImages: true); $activity = StartService::run($this->service, pullLatestImages: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function forceDeploy() public function forceDeploy()
{ {
$this->authorizeService('deploy');
try { try {
$this->authorizeService('deploy');
$activities = Activity::where('properties->type_uuid', $this->service->uuid) $activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) { ->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value) $q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
@ -131,27 +134,27 @@ public function forceDeploy()
$activity->save(); $activity->save();
} }
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Exception $e) { } catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage()); return handleError($e, $this);
} }
} }
public function stop() public function stop()
{ {
$this->authorizeService('stop');
try { try {
$this->authorizeService('stop');
StopService::dispatch($this->service, false, $this->docker_cleanup); StopService::dispatch($this->service, false, $this->docker_cleanup);
} catch (\Exception $e) { } catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage()); return handleError($e, $this);
} }
} }
public function restart() public function restart()
{ {
try {
$this->authorizeService('deploy'); $this->authorizeService('deploy');
$this->checkDeployments(); $this->checkDeployments();
if ($this->isDeploymentProgress) { if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.'); $this->dispatch('error', 'There is a deployment in progress.');
@ -159,13 +162,17 @@ public function restart()
return; return;
} }
$activity = StartService::run($this->service, stopBeforeStart: true); $activity = StartService::run($this->service, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function pullAndRestartEvent() public function pullAndRestartEvent()
{ {
try {
$this->authorizeService('deploy'); $this->authorizeService('deploy');
$this->checkDeployments(); $this->checkDeployments();
if ($this->isDeploymentProgress) { if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.'); $this->dispatch('error', 'There is a deployment in progress.');
@ -173,7 +180,11 @@ public function pullAndRestartEvent()
return; return;
} }
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
private function authorizeService(string $ability): void private function authorizeService(string $ability): void

View file

@ -4,16 +4,21 @@
use App\Models\Service; use App\Models\Service;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
class StackForm extends Component class StackForm extends Component
{ {
use AuthorizesRequests;
public Service $service; public Service $service;
public Collection $fields; public Collection $fields;
public bool $isPasswordHiddenForMember = false;
protected $listeners = ['saveCompose']; protected $listeners = ['saveCompose'];
// Explicit properties // Explicit properties
@ -118,6 +123,17 @@ public function mount()
})->flatMap(function ($group) { })->flatMap(function ($group) {
return $group; return $group;
}); });
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->fields = $this->fields->map(function ($field) {
if (data_get($field, 'isPassword')) {
$field['value'] = null;
}
return $field;
});
}
} }
public function saveCompose($raw) public function saveCompose($raw)
@ -128,14 +144,20 @@ public function saveCompose($raw)
public function instantSave() public function instantSave()
{ {
try {
$this->authorize('update', $this->service);
$this->syncData(true); $this->syncData(true);
$this->service->save(); $this->service->save();
$this->dispatch('success', 'Service settings saved.'); $this->dispatch('success', 'Service settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function submit($notify = true) public function submit($notify = true)
{ {
try { try {
$this->authorize('update', $this->service);
$this->validate(); $this->validate();
$this->syncData(true); $this->syncData(true);

View file

@ -8,7 +8,6 @@
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Danger extends Component class Danger extends Component
{ {
@ -39,7 +38,7 @@ class Danger extends Component
public function mount() public function mount()
{ {
$parameters = get_route_parameters(); $parameters = get_route_parameters();
$this->modalId = new Cuid2; $this->modalId = new_public_id();
$this->projectUuid = data_get($parameters, 'project_uuid'); $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentUuid = data_get($parameters, 'environment_uuid'); $this->environmentUuid = data_get($parameters, 'environment_uuid');

View file

@ -7,12 +7,14 @@
use App\Events\ApplicationStatusChanged; use App\Events\ApplicationStatusChanged;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Destination extends Component class Destination extends Component
{ {
use AuthorizesRequests;
public $resource; public $resource;
public Collection $networks; public Collection $networks;
@ -59,6 +61,7 @@ public function loadData()
public function stop($serverId) public function stop($serverId)
{ {
try { try {
$this->authorize('deploy', $this->resource);
$server = Server::ownedByCurrentTeam()->findOrFail($serverId); $server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server); StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers(); $this->refreshServers();
@ -70,12 +73,13 @@ public function stop($serverId)
public function redeploy(int $network_id, int $server_id) public function redeploy(int $network_id, int $server_id)
{ {
try { try {
$this->authorize('deploy', $this->resource);
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) { if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>'); $this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return; return;
} }
$deployment_uuid = new Cuid2; $deployment_uuid = new_public_id();
$server = Server::ownedByCurrentTeam()->findOrFail($server_id); $server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail(); $destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
$result = queue_application_deployment( $result = queue_application_deployment(
@ -128,7 +132,7 @@ public function promote(int $network_id, int $server_id)
}); });
$this->resource->refresh(); $this->resource->refresh();
$this->refreshServers(); $this->refreshServers();
} catch (\Exception $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
@ -149,7 +153,7 @@ public function addServer(int $network_id, int $server_id)
$this->resource->additional_networks()->attach($network->id, ['server_id' => $server->id]); $this->resource->additional_networks()->attach($network->id, ['server_id' => $server->id]);
$this->dispatch('refresh'); $this->dispatch('refresh');
} catch (\Exception $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
@ -157,6 +161,7 @@ public function addServer(int $network_id, int $server_id)
public function removeServer(int $network_id, int $server_id, $password, $selectedActions = []) public function removeServer(int $network_id, int $server_id, $password, $selectedActions = [])
{ {
try { try {
$this->authorize('update', $this->resource);
if (! verifyPasswordConfirmation($password, $this)) { if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.'; return 'The provided password is incorrect.';
} }

View file

@ -111,7 +111,7 @@ private function getEnvironmentVariables(bool $isPreview, bool $withSearch = tru
$query->orderBy('order'); $query->orderBy('order');
} }
return $query->get(); return $this->nullLockedValues($query->get());
} }
private function searchTerm(): string private function searchTerm(): string
@ -127,6 +127,20 @@ public function getHasEnvironmentVariablesProperty(): bool
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty(); $this->hardcodedEnvironmentVariablesPreview->isNotEmpty();
} }
private function nullLockedValues($envs)
{
$isMember = auth()->user()?->isMember();
$envs->each(function ($env) use ($isMember) {
if ($env->is_shown_once || $isMember) {
$env->value = null;
$env->real_value = null;
}
});
return $envs;
}
public function getIsSearchActiveProperty(): bool public function getIsSearchActiveProperty(): bool
{ {
return $this->searchTerm() !== ''; return $this->searchTerm() !== '';
@ -204,7 +218,12 @@ public function getDevView()
private function formatEnvironmentVariables($variables) private function formatEnvironmentVariables($variables)
{ {
return $variables->map(function ($item) { $isMember = auth()->user()?->isMember();
return $variables->map(function ($item) use ($isMember) {
if ($isMember) {
return "$item->key=(Hidden, only admins can view)";
}
if ($item->is_shown_once) { if ($item->is_shown_once) {
return "$item->key=(Locked Secret, delete and add again to change)"; return "$item->key=(Locked Secret, delete and add again to change)";
} }

View file

@ -61,6 +61,8 @@ class Show extends Component
public bool $is_redis_credential = false; public bool $is_redis_credential = false;
public bool $isValueHidden = false;
public array $problematicVariables = []; public array $problematicVariables = [];
protected $listeners = [ protected $listeners = [
@ -160,6 +162,13 @@ public function syncData(bool $toModel = false)
$this->is_really_required = $this->env->is_really_required ?? false; $this->is_really_required = $this->env->is_really_required ?? false;
$this->is_shared = $this->env->is_shared ?? false; $this->is_shared = $this->env->is_shared ?? false;
$this->real_value = $this->env->real_value; $this->real_value = $this->env->real_value;
if ($this->env->is_shown_once || auth()->user()?->isMember()) {
$this->value = null;
$this->real_value = null;
}
$this->isValueHidden = auth()->user()?->isMember() ?? false;
} }
} }

View file

@ -6,12 +6,15 @@
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class ExecuteContainerCommand extends Component class ExecuteContainerCommand extends Component
{ {
use AuthorizesRequests;
public $selected_container = 'default'; public $selected_container = 'default';
public Collection $containers; public Collection $containers;
@ -40,6 +43,7 @@ public function mount()
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application'; $this->type = 'application';
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail(); $this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
if ($this->resource->destination->server->isFunctional()) { if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server); $this->servers = $this->servers->push($this->resource->destination->server);
} }
@ -56,6 +60,7 @@ public function mount()
abort(404); abort(404);
} }
$this->resource = $resource; $this->resource = $resource;
$this->authorize('view', $this->resource);
if ($this->resource->destination->server->isFunctional()) { if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server); $this->servers = $this->servers->push($this->resource->destination->server);
} }
@ -63,6 +68,7 @@ public function mount()
} elseif (data_get($this->parameters, 'service_uuid')) { } elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
if ($this->resource->server->isFunctional()) { if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server); $this->servers = $this->servers->push($this->resource->server);
} }
@ -70,6 +76,7 @@ public function mount()
} elseif (data_get($this->parameters, 'server_uuid')) { } elseif (data_get($this->parameters, 'server_uuid')) {
$this->type = 'server'; $this->type = 'server';
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail(); $this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
$this->servers = $this->servers->push($this->resource); $this->servers = $this->servers->push($this->resource);
} }
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled()); $this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
@ -152,7 +159,9 @@ public function updatedSelectedContainer()
public function connectToServer() public function connectToServer()
{ {
try { try {
$this->authorize('canAccessTerminal');
$server = $this->servers->first(); $server = $this->servers->first();
$this->authorize('view', $server);
if ($server->isForceDisabled()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); throw new \RuntimeException('Server is disabled.');
} }
@ -181,6 +190,7 @@ public function connectToContainer()
return; return;
} }
try { try {
$this->authorize('canAccessTerminal');
// Validate container name format // Validate container name format
if (! ValidationPatterns::isValidContainerName($this->selected_container)) { if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
throw new \InvalidArgumentException('Invalid container name format'); throw new \InvalidArgumentException('Invalid container name format');
@ -198,6 +208,8 @@ public function connectToContainer()
throw new \RuntimeException('Invalid server configuration.'); throw new \RuntimeException('Invalid server configuration.');
} }
$this->authorize('view', $server);
if ($server->isForceDisabled()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); throw new \RuntimeException('Server is disabled.');
} }

View file

@ -78,8 +78,12 @@ class HealthChecks extends Component
public function mount() public function mount()
{ {
try {
$this->authorize('view', $this->resource); $this->authorize('view', $this->resource);
$this->syncData(); $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function syncData(bool $toModel = false): void public function syncData(bool $toModel = false): void

View file

@ -22,7 +22,6 @@
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class ResourceOperations extends Component class ResourceOperations extends Component
{ {
@ -56,6 +55,7 @@ public function toggleVolumeCloning(bool $value)
public function cloneTo($destination_id) public function cloneTo($destination_id)
{ {
try {
$this->authorize('update', $this->resource); $this->authorize('update', $this->resource);
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id); $new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
@ -65,7 +65,7 @@ public function cloneTo($destination_id)
if (! $new_destination) { if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.'); return $this->addError('destination_id', 'Destination not found.');
} }
$uuid = (string) new Cuid2; $uuid = new_public_id();
$server = $new_destination->server; $server = $new_destination->server;
if ($this->resource->getMorphClass() === Application::class) { if ($this->resource->getMorphClass() === Application::class) {
@ -88,7 +88,7 @@ public function cloneTo($destination_id)
$this->resource->getMorphClass() === StandaloneDragonfly::class || $this->resource->getMorphClass() === StandaloneDragonfly::class ||
$this->resource->getMorphClass() === StandaloneClickhouse::class $this->resource->getMorphClass() === StandaloneClickhouse::class
) { ) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$new_resource = $this->resource->replicate([ $new_resource = $this->resource->replicate([
'id', 'id',
'created_at', 'created_at',
@ -179,7 +179,7 @@ public function cloneTo($destination_id)
$scheduledBackups = $this->resource->scheduledBackups()->get(); $scheduledBackups = $this->resource->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) { foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$newBackup = $backup->replicate([ $newBackup = $backup->replicate([
'id', 'id',
'created_at', 'created_at',
@ -215,7 +215,7 @@ public function cloneTo($destination_id)
return redirect()->to($route); return redirect()->to($route);
} elseif ($this->resource->type() === 'service') { } elseif ($this->resource->type() === 'service') {
$uuid = (string) new Cuid2; $uuid = new_public_id();
$new_resource = $this->resource->replicate([ $new_resource = $this->resource->replicate([
'id', 'id',
'created_at', 'created_at',
@ -225,7 +225,7 @@ public function cloneTo($destination_id)
'name' => $this->resource->name.'-clone-'.$uuid, 'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id, 'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(), 'destination_type' => $new_destination->getMorphClass(),
'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column) 'server_id' => $new_destination->server_id,
]); ]);
$new_resource->save(); $new_resource->save();
@ -242,7 +242,7 @@ public function cloneTo($destination_id)
'created_at', 'created_at',
'updated_at', 'updated_at',
])->fill([ ])->fill([
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
'service_id' => $new_resource->id, 'service_id' => $new_resource->id,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ]);
@ -358,6 +358,9 @@ public function cloneTo($destination_id)
return redirect()->to($route); return redirect()->to($route);
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function moveTo($environment_id) public function moveTo($environment_id)

View file

@ -5,11 +5,14 @@
use App\Helpers\SshMultiplexingHelper; use App\Helpers\SshMultiplexingHelper;
use App\Models\Server; use App\Models\Server;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class Terminal extends Component class Terminal extends Component
{ {
use AuthorizesRequests;
public bool $hasShell = true; public bool $hasShell = true;
public bool $isTerminalConnected = false; public bool $isTerminalConnected = false;
@ -32,7 +35,11 @@ private function checkShellAvailability(Server $server, string $container): bool
#[On('send-terminal-command')] #[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid) public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{ {
$this->authorize('canAccessTerminal');
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
$this->authorize('view', $server);
if (! $server->isTerminalEnabled() || $server->isForceDisabled()) { if (! $server->isTerminalEnabled() || $server->isForceDisabled()) {
abort(403, 'Terminal access is disabled on this server.'); abort(403, 'Terminal access is disabled on this server.');
} }

View file

@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Models\Application; use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class UploadConfig extends Component class UploadConfig extends Component
{ {
use AuthorizesRequests;
public $config; public $config;
public $applicationId; public $applicationId;
@ -29,13 +32,12 @@ public function mount()
public function uploadConfig() public function uploadConfig()
{ {
try { try {
$application = Application::findOrFail($this->applicationId); $application = Application::ownedByCurrentTeam()->findOrFail($this->applicationId);
$this->authorize('update', $application);
$application->setConfig($this->config); $application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated'); $this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) { } catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage()); return handleError($e, $this);
return;
} }
} }

View file

@ -34,19 +34,24 @@ public function mount()
{ {
$this->deploywebhook = generateDeployWebhook($this->resource); $this->deploywebhook = generateDeployWebhook($this->resource);
if ($this->canViewSecrets()) {
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github'); $this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab'); $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket'); $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea'); $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
}
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
$this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
} }
public function canViewSecrets(): bool
{
return auth()->user()->can('update', $this->resource);
}
public function submit() public function submit()
{ {
try { try {

View file

@ -5,11 +5,13 @@
use App\Models\Environment; use App\Models\Environment;
use App\Models\Project; use App\Models\Project;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component class Show extends Component
{ {
use AuthorizesRequests;
public Project $project; public Project $project;
public string $name; public string $name;
@ -41,11 +43,12 @@ public function mount(string $project_uuid)
public function submit() public function submit()
{ {
try { try {
$this->authorize('create', Environment::class);
$this->validate(); $this->validate();
$environment = Environment::create([ $environment = Environment::create([
'name' => $this->name, 'name' => $this->name,
'project_id' => $this->project->id, 'project_id' => $this->project->id,
'uuid' => (string) new Cuid2, 'uuid' => new_public_id(),
]); ]);
return redirectRoute($this, 'project.resource.index', [ return redirectRoute($this, 'project.resource.index', [

View file

@ -36,6 +36,12 @@ class ApiTokens extends Component
#[Locked] #[Locked]
public bool $canUseWritePermissions = false; public bool $canUseWritePermissions = false;
#[Locked]
public bool $canUseDeployPermissions = false;
#[Locked]
public bool $canUseSensitivePermissions = false;
public function render() public function render()
{ {
return view('livewire.security.api-tokens'); return view('livewire.security.api-tokens');
@ -46,6 +52,8 @@ public function mount()
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled; $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class); $this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class);
$this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class); $this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class);
$this->canUseDeployPermissions = auth()->user()->can('useDeployPermissions', PersonalAccessToken::class);
$this->canUseSensitivePermissions = auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class);
$this->getTokens(); $this->getTokens();
} }
@ -56,10 +64,9 @@ private function getTokens()
public function updatedPermissions($permissionToUpdate) public function updatedPermissions($permissionToUpdate)
{ {
// Check if user is trying to use restricted permissions // Re-evaluate policies fresh — never trust stored snapshot booleans.
if ($permissionToUpdate == 'root' && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) { if ($permissionToUpdate == 'root' && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use root permissions.'); $this->dispatch('error', 'You do not have permission to use root permissions.');
// Remove root from permissions if it was somehow added
$this->permissions = array_diff($this->permissions, ['root']); $this->permissions = array_diff($this->permissions, ['root']);
return; return;
@ -67,12 +74,25 @@ public function updatedPermissions($permissionToUpdate)
if (in_array($permissionToUpdate, ['write', 'write:sensitive'], true) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) { if (in_array($permissionToUpdate, ['write', 'write:sensitive'], true) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use write permissions.'); $this->dispatch('error', 'You do not have permission to use write permissions.');
// Remove write permissions if they were somehow added
$this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']); $this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']);
return; return;
} }
if ($permissionToUpdate == 'deploy' && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use deploy permissions.');
$this->permissions = array_diff($this->permissions, ['deploy']);
return;
}
if ($permissionToUpdate == 'read:sensitive' && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
$this->dispatch('error', 'You do not have permission to use read:sensitive permissions.');
$this->permissions = array_diff($this->permissions, ['read:sensitive']);
return;
}
if ($permissionToUpdate == 'root') { if ($permissionToUpdate == 'root') {
$this->permissions = ['root']; $this->permissions = ['root'];
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions, true)) { } elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions, true)) {
@ -92,7 +112,9 @@ public function addNewToken()
try { try {
$this->authorize('create', PersonalAccessToken::class); $this->authorize('create', PersonalAccessToken::class);
// Validate permissions based on user role // Re-evaluate policies fresh against the current authenticated user.
// Never trust $this->canUse* booleans — they come from the Livewire
// snapshot which can be replayed from another user's session.
if (in_array('root', $this->permissions, true) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) { if (in_array('root', $this->permissions, true) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with root permissions.'); throw new \Exception('You do not have permission to create tokens with root permissions.');
} }
@ -101,6 +123,14 @@ public function addNewToken()
throw new \Exception('You do not have permission to create tokens with write permissions.'); throw new \Exception('You do not have permission to create tokens with write permissions.');
} }
if (in_array('deploy', $this->permissions, true) && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with deploy permissions.');
}
if (in_array('read:sensitive', $this->permissions, true) && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
throw new \Exception('You do not have permission to create tokens with read:sensitive permissions.');
}
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
'expiresInDays' => 'nullable|integer|in:7,30,60,90,365', 'expiresInDays' => 'nullable|integer|in:7,30,60,90,365',
@ -108,6 +138,7 @@ public function addNewToken()
$expiresAt = $this->expiresInDays ? now()->addDays($this->expiresInDays) : null; $expiresAt = $this->expiresInDays ? now()->addDays($this->expiresInDays) : null;
$token = auth()->user()->createToken($this->description, array_values($this->permissions), $expiresAt); $token = auth()->user()->createToken($this->description, array_values($this->permissions), $expiresAt);
$this->getTokens(); $this->getTokens();
// Do NOT strip the numeric prefix (e.g. "69|...") — Sanctum uses it to index and look up tokens.
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View file

@ -3,6 +3,7 @@
namespace App\Livewire\Security; namespace App\Livewire\Security;
use App\Models\CloudInitScript; use App\Models\CloudInitScript;
use App\Rules\ValidCloudInitYaml;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
@ -20,6 +21,7 @@ class CloudInitScriptForm extends Component
public function mount(?int $scriptId = null) public function mount(?int $scriptId = null)
{ {
try {
if ($scriptId) { if ($scriptId) {
$this->scriptId = $scriptId; $this->scriptId = $scriptId;
$cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
@ -30,13 +32,16 @@ public function mount(?int $scriptId = null)
} else { } else {
$this->authorize('create', CloudInitScript::class); $this->authorize('create', CloudInitScript::class);
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
protected function rules(): array protected function rules(): array
{ {
return [ return [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'script' => ['required', 'string', new \App\Rules\ValidCloudInitYaml], 'script' => ['required', 'string', new ValidCloudInitYaml],
]; ];
} }
@ -64,17 +69,29 @@ public function save()
'script' => $this->script, 'script' => $this->script,
]); ]);
auditLog('ui.cloud_init_script.updated', [
'team_id' => currentTeam()->id,
'cloud_init_script_id' => $cloudInitScript->id,
'cloud_init_script_name' => $cloudInitScript->name,
]);
$message = 'Cloud-init script updated successfully.'; $message = 'Cloud-init script updated successfully.';
} else { } else {
// Create new script // Create new script
$this->authorize('create', CloudInitScript::class); $this->authorize('create', CloudInitScript::class);
CloudInitScript::create([ $cloudInitScript = CloudInitScript::create([
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'name' => $this->name, 'name' => $this->name,
'script' => $this->script, 'script' => $this->script,
]); ]);
auditLog('ui.cloud_init_script.created', [
'team_id' => currentTeam()->id,
'cloud_init_script_id' => $cloudInitScript->id,
'cloud_init_script_name' => $cloudInitScript->name,
]);
$message = 'Cloud-init script created successfully.'; $message = 'Cloud-init script created successfully.';
} }

View file

@ -36,9 +36,16 @@ public function deleteScript(int $scriptId)
$script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
$this->authorize('delete', $script); $this->authorize('delete', $script);
$scriptName = $script->name;
$script->delete(); $script->delete();
$this->loadScripts(); $this->loadScripts();
auditLog('ui.cloud_init_script.deleted', [
'team_id' => currentTeam()->id,
'cloud_init_script_id' => $scriptId,
'cloud_init_script_name' => $scriptName,
]);
$this->dispatch('success', 'Cloud-init script deleted successfully.'); $this->dispatch('success', 'Cloud-init script deleted successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View file

@ -21,7 +21,11 @@ class CloudProviderTokenForm extends Component
public function mount() public function mount()
{ {
try {
$this->authorize('create', CloudProviderToken::class); $this->authorize('create', CloudProviderToken::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
protected function rules(): array protected function rules(): array
@ -50,7 +54,6 @@ private function validateToken(string $provider, string $token): bool
$response = Http::withHeaders([ $response = Http::withHeaders([
'Authorization' => 'Bearer '.$token, 'Authorization' => 'Bearer '.$token,
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
ray($response);
return $response->successful(); return $response->successful();
} }
@ -81,6 +84,13 @@ public function addToken()
'name' => $this->name, 'name' => $this->name,
]); ]);
auditLog('ui.cloud_token.created', [
'team_id' => currentTeam()->id,
'cloud_token_uuid' => $savedToken->uuid,
'cloud_token_name' => $savedToken->name,
'provider' => $savedToken->provider,
]);
$this->reset(['token', 'name']); $this->reset(['token', 'name']);
// Dispatch event with token ID so parent components can react // Dispatch event with token ID so parent components can react

View file

@ -4,6 +4,7 @@
use App\Models\CloudProviderToken; use App\Models\CloudProviderToken;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class CloudProviderTokens extends Component class CloudProviderTokens extends Component
@ -14,8 +15,12 @@ class CloudProviderTokens extends Component
public function mount() public function mount()
{ {
try {
$this->authorize('viewAny', CloudProviderToken::class); $this->authorize('viewAny', CloudProviderToken::class);
$this->loadTokens(); $this->loadTokens();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function getListeners() public function getListeners()
@ -53,6 +58,14 @@ public function validateToken(int $tokenId)
} else { } else {
$this->dispatch('error', 'Unknown provider.'); $this->dispatch('error', 'Unknown provider.');
} }
auditLog('ui.cloud_token.validated', [
'team_id' => currentTeam()->id,
'cloud_token_uuid' => $token->uuid,
'cloud_token_name' => $token->name,
'provider' => $token->provider,
'valid' => $isValid ?? false,
]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@ -61,7 +74,7 @@ public function validateToken(int $tokenId)
private function validateHetznerToken(string $token): bool private function validateHetznerToken(string $token): bool
{ {
try { try {
$response = \Illuminate\Support\Facades\Http::withToken($token) $response = Http::withToken($token)
->timeout(10) ->timeout(10)
->get('https://api.hetzner.cloud/v1/servers?per_page=1'); ->get('https://api.hetzner.cloud/v1/servers?per_page=1');
@ -74,7 +87,7 @@ private function validateHetznerToken(string $token): bool
private function validateDigitalOceanToken(string $token): bool private function validateDigitalOceanToken(string $token): bool
{ {
try { try {
$response = \Illuminate\Support\Facades\Http::withToken($token) $response = Http::withToken($token)
->timeout(10) ->timeout(10)
->get('https://api.digitalocean.com/v2/account'); ->get('https://api.digitalocean.com/v2/account');
@ -98,9 +111,19 @@ public function deleteToken(int $tokenId)
return; return;
} }
$tokenUuid = $token->uuid;
$tokenName = $token->name;
$tokenProvider = $token->provider;
$token->delete(); $token->delete();
$this->loadTokens(); $this->loadTokens();
auditLog('ui.cloud_token.deleted', [
'team_id' => currentTeam()->id,
'cloud_token_uuid' => $tokenUuid,
'cloud_token_name' => $tokenName,
'provider' => $tokenProvider,
]);
$this->dispatch('success', 'Cloud provider token deleted successfully.'); $this->dispatch('success', 'Cloud provider token deleted successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View file

@ -21,8 +21,12 @@ public function render()
public function cleanupUnusedKeys() public function cleanupUnusedKeys()
{ {
try {
$this->authorize('create', PrivateKey::class); $this->authorize('create', PrivateKey::class);
PrivateKey::cleanupUnusedKeys(); PrivateKey::cleanupUnusedKeys();
$this->dispatch('success', 'Unused keys have been cleaned up.'); $this->dispatch('success', 'Unused keys have been cleaned up.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View file

@ -5,6 +5,7 @@
use App\Models\CloudProviderToken; use App\Models\CloudProviderToken;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@ -67,6 +68,16 @@ public function setCloudProviderToken($tokenId)
$this->server->cloudProviderToken()->associate($ownedToken); $this->server->cloudProviderToken()->associate($ownedToken);
$this->server->save(); $this->server->save();
auditLog('ui.server.cloud_token_assigned', [
'team_id' => currentTeam()->id,
'server_uuid' => $this->server->uuid,
'server_name' => $this->server->name,
'cloud_token_uuid' => $ownedToken->uuid,
'cloud_token_name' => $ownedToken->name,
'provider' => $ownedToken->provider,
]);
$this->dispatch('success', 'Hetzner token updated successfully.'); $this->dispatch('success', 'Hetzner token updated successfully.');
$this->dispatch('refreshServerShow'); $this->dispatch('refreshServerShow');
} catch (\Exception $e) { } catch (\Exception $e) {
@ -79,7 +90,7 @@ private function validateTokenForServer(CloudProviderToken $token): array
{ {
try { try {
// First, validate the token itself // First, validate the token itself
$response = \Illuminate\Support\Facades\Http::withHeaders([ $response = Http::withHeaders([
'Authorization' => 'Bearer '.$token->token, 'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
@ -92,7 +103,7 @@ private function validateTokenForServer(CloudProviderToken $token): array
// Check if this token can access the specific Hetzner server // Check if this token can access the specific Hetzner server
if ($this->server->hetzner_server_id) { if ($this->server->hetzner_server_id) {
$serverResponse = \Illuminate\Support\Facades\Http::withHeaders([ $serverResponse = Http::withHeaders([
'Authorization' => 'Bearer '.$token->token, 'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}"); ])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}");
@ -123,7 +134,7 @@ public function validateToken()
return; return;
} }
$response = \Illuminate\Support\Facades\Http::withHeaders([ $response = Http::withHeaders([
'Authorization' => 'Bearer '.$token->token, 'Authorization' => 'Bearer '.$token->token,
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
@ -132,6 +143,16 @@ public function validateToken()
} else { } else {
$this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.'); $this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.');
} }
auditLog('ui.server.cloud_token_validated', [
'team_id' => currentTeam()->id,
'server_uuid' => $this->server->uuid,
'server_name' => $this->server->name,
'cloud_token_uuid' => $token->uuid,
'cloud_token_name' => $token->name,
'provider' => $token->provider,
'valid' => $response->successful(),
]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View file

@ -72,12 +72,16 @@ public function toggleCloudflareTunnels()
public function manualCloudflareConfig() public function manualCloudflareConfig()
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->isCloudflareTunnelsEnabled = true; $this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true; $this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save(); $this->server->settings->save();
$this->server->refresh(); $this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnel enabled.'); $this->dispatch('success', 'Cloudflare Tunnel enabled.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function automatedCloudflareConfig() public function automatedCloudflareConfig()

View file

@ -69,6 +69,11 @@ public function add($name)
public function scan() public function scan()
{ {
try {
$this->authorize('update', $this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers; $alreadyAddedNetworks = $this->server->swarmDockers;
} else { } else {

View file

@ -79,6 +79,7 @@ class ByHetzner extends Component
public function mount() public function mount()
{ {
try {
$this->authorize('viewAny', CloudProviderToken::class); $this->authorize('viewAny', CloudProviderToken::class);
$this->loadTokens(); $this->loadTokens();
$this->loadSavedCloudInitScripts(); $this->loadSavedCloudInitScripts();
@ -88,6 +89,9 @@ public function mount()
if ($this->private_keys->count() > 0) { if ($this->private_keys->count() > 0) {
$this->private_key_id = $this->private_keys->first()->id; $this->private_key_id = $this->private_keys->first()->id;
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function loadSavedCloudInitScripts() public function loadSavedCloudInitScripts()

View file

@ -102,11 +102,15 @@ public function getConfigurationFilePathProperty(): string
public function changeProxy() public function changeProxy()
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->server->proxy = null; $this->server->proxy = null;
$this->server->save(); $this->server->save();
$this->dispatch('reloadWindow'); $this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function selectProxy($proxy_type) public function selectProxy($proxy_type)

View file

@ -22,6 +22,7 @@ class DynamicConfigurationNavbar extends Component
public function delete(string $fileName) public function delete(string $fileName)
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$proxy_path = $this->server->proxyPath(); $proxy_path = $this->server->proxyPath();
$proxy_type = $this->server->proxyType(); $proxy_type = $this->server->proxyType();
@ -49,6 +50,9 @@ public function delete(string $fileName)
$this->dispatch('success', 'File deleted.'); $this->dispatch('success', 'File deleted.');
$this->dispatch('loadDynamicConfigurations'); $this->dispatch('loadDynamicConfigurations');
$this->dispatch('refresh'); $this->dispatch('refresh');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View file

@ -3,11 +3,14 @@
namespace App\Livewire\Server\Proxy; namespace App\Livewire\Server\Proxy;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class DynamicConfigurations extends Component class DynamicConfigurations extends Component
{ {
use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public $parameters = []; public $parameters = [];
@ -35,6 +38,11 @@ public function initLoadDynamicConfigurations()
public function loadDynamicConfigurations() public function loadDynamicConfigurations()
{ {
try {
$this->authorize('view', $this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$proxy_path = $this->server->proxyPath(); $proxy_path = $this->server->proxyPath();
$files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server); $files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server);
$files = collect(explode("\n", $files))->filter(fn ($file) => ! empty($file)); $files = collect(explode("\n", $files))->filter(fn ($file) => ! empty($file));

View file

@ -30,6 +30,8 @@ public function getListeners()
public function startUnmanaged($id) public function startUnmanaged($id)
{ {
try {
$this->authorize('update', $this->server);
if (! ValidationPatterns::isValidContainerName($id)) { if (! ValidationPatterns::isValidContainerName($id)) {
$this->dispatch('error', 'Invalid container identifier.'); $this->dispatch('error', 'Invalid container identifier.');
@ -38,10 +40,15 @@ public function startUnmanaged($id)
$this->server->startUnmanaged($id); $this->server->startUnmanaged($id);
$this->dispatch('success', 'Container started.'); $this->dispatch('success', 'Container started.');
$this->loadUnmanagedContainers(); $this->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function restartUnmanaged($id) public function restartUnmanaged($id)
{ {
try {
$this->authorize('update', $this->server);
if (! ValidationPatterns::isValidContainerName($id)) { if (! ValidationPatterns::isValidContainerName($id)) {
$this->dispatch('error', 'Invalid container identifier.'); $this->dispatch('error', 'Invalid container identifier.');
@ -50,10 +57,15 @@ public function restartUnmanaged($id)
$this->server->restartUnmanaged($id); $this->server->restartUnmanaged($id);
$this->dispatch('success', 'Container restarted.'); $this->dispatch('success', 'Container restarted.');
$this->loadUnmanagedContainers(); $this->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function stopUnmanaged($id) public function stopUnmanaged($id)
{ {
try {
$this->authorize('update', $this->server);
if (! ValidationPatterns::isValidContainerName($id)) { if (! ValidationPatterns::isValidContainerName($id)) {
$this->dispatch('error', 'Invalid container identifier.'); $this->dispatch('error', 'Invalid container identifier.');
@ -62,6 +74,9 @@ public function stopUnmanaged($id)
$this->server->stopUnmanaged($id); $this->server->stopUnmanaged($id);
$this->dispatch('success', 'Container stopped.'); $this->dispatch('success', 'Container stopped.');
$this->loadUnmanagedContainers(); $this->loadUnmanagedContainers();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function refreshStatus() public function refreshStatus()

View file

@ -41,7 +41,11 @@ public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail(); $this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail();
try {
$this->authorize('viewSecurity', $this->server); $this->authorize('viewSecurity', $this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function checkForUpdatesDispatch() public function checkForUpdatesDispatch()
@ -69,6 +73,7 @@ public function checkForUpdates()
public function updateAllPackages() public function updateAllPackages()
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
if (! $this->packageManager || ! $this->osId) { if (! $this->packageManager || ! $this->osId) {
$this->dispatch('error', message: 'Run "Check for updates" first.'); $this->dispatch('error', message: 'Run "Check for updates" first.');
@ -76,7 +81,6 @@ public function updateAllPackages()
return; return;
} }
try {
$activity = UpdatePackage::run( $activity = UpdatePackage::run(
server: $this->server, server: $this->server,
packageManager: $this->packageManager, packageManager: $this->packageManager,
@ -84,8 +88,8 @@ public function updateAllPackages()
all: true all: true
); );
$this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class); $this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class);
} catch (\Exception $e) { } catch (\Throwable $e) {
$this->dispatch('error', message: $e->getMessage()); return handleError($e, $this);
} }
} }

View file

@ -3,11 +3,14 @@
namespace App\Livewire\Server\Sentinel; namespace App\Livewire\Server\Sentinel;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
class Logs extends Component class Logs extends Component
{ {
use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public array $parameters = []; public array $parameters = [];
@ -19,7 +22,11 @@ public function mount(): void
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);
return;
} }
$this->authorize('viewSentinel', $this->server);
} }
public function render(): View public function render(): View

View file

@ -3,11 +3,14 @@
namespace App\Livewire\Server\Sentinel; namespace App\Livewire\Server\Sentinel;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public array $parameters = []; public array $parameters = [];
@ -19,7 +22,11 @@ public function mount(): void
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) { } catch (\Throwable $e) {
handleError($e, $this); handleError($e, $this);
return;
} }
$this->authorize('viewSentinel', $this->server);
} }
public function render(): View public function render(): View

View file

@ -299,6 +299,7 @@ public function validateServer($install = true)
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
try {
$this->syncData(true); $this->syncData(true);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
@ -312,6 +313,9 @@ public function checkLocalhostConnection()
return; return;
} }
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function restartSentinel() public function restartSentinel()
@ -413,6 +417,7 @@ public function instantSave()
public function checkHetznerServerStatus(bool $manual = false) public function checkHetznerServerStatus(bool $manual = false)
{ {
try { try {
$this->authorize('view', $this->server);
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) { if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.'); $this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');
@ -480,6 +485,7 @@ public function handleServerValidated($event = null)
public function startHetznerServer() public function startHetznerServer()
{ {
try { try {
$this->authorize('update', $this->server);
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) { if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.'); $this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');

View file

@ -72,6 +72,7 @@ public function startValidatingAfterAsking()
public function retry() public function retry()
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->uptime = null; $this->uptime = null;
$this->supported_os_type = null; $this->supported_os_type = null;
@ -82,10 +83,14 @@ public function retry()
$this->error = null; $this->error = null;
$this->number_of_tries = 0; $this->number_of_tries = 0;
$this->init(); $this->init();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function validateConnection() public function validateConnection()
{ {
try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) { if (! $this->uptime) {
@ -98,6 +103,9 @@ public function validateConnection()
return; return;
} }
$this->dispatch('validateOS'); $this->dispatch('validateOS');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function validateOS() public function validateOS()

View file

@ -5,11 +5,14 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Rules\ValidDnsServers; use App\Rules\ValidDnsServers;
use App\Rules\ValidIpOrCidr; use App\Rules\ValidIpOrCidr;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
class Advanced extends Component class Advanced extends Component
{ {
use AuthorizesRequests;
public InstanceSettings $settings; public InstanceSettings $settings;
#[Validate('boolean')] #[Validate('boolean')]
@ -77,6 +80,7 @@ public function mount()
public function submit() public function submit()
{ {
try { try {
$this->authorize('update', $this->settings);
$this->validate(); $this->validate();
$this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim(); $this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim();
@ -146,6 +150,7 @@ public function submit()
public function instantSave() public function instantSave()
{ {
try { try {
$this->authorize('update', $this->settings);
$this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->do_not_track = $this->do_not_track; $this->settings->do_not_track = $this->do_not_track;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
@ -178,6 +183,7 @@ public function toggleRegistration($password): bool
public function toggleTwoStepConfirmation($password): bool public function toggleTwoStepConfirmation($password): bool
{ {
$this->authorize('update', $this->settings);
if (! verifyPasswordConfirmation($password, $this)) { if (! verifyPasswordConfirmation($password, $this)) {
return false; return false;
} }

Some files were not shown because too many files have changed in this diff Show more