diff --git a/app/config/collections/common.php b/app/config/collections/common.php index e77d5403d2..6de7eb224b 100644 --- a/app/config/collections/common.php +++ b/app/config/collections/common.php @@ -1439,13 +1439,6 @@ return [ 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], - [ - '$id' => ID::custom('_key_roles'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['roles'], - 'lengths' => [128], - 'orders' => [], - ], ], ], diff --git a/app/config/errors.php b/app/config/errors.php index 6d9d29a8ea..8365e8c705 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -393,6 +393,16 @@ return [ 'description' => 'Membership is already confirmed.', 'code' => 409, ], + Exception::MEMBERSHIP_DELETION_PROHIBITED => [ + 'name' => Exception::MEMBERSHIP_DELETION_PROHIBITED, + 'description' => 'Membership deletion is prohibited.', + 'code' => 400, + ], + Exception::MEMBERSHIP_DOWNGRADE_PROHIBITED => [ + 'name' => Exception::MEMBERSHIP_DOWNGRADE_PROHIBITED, + 'description' => 'Membership role downgrade is prohibited.', + 'code' => 400, + ], /** Avatars */ Exception::AVATAR_SET_NOT_FOUND => [ diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 49d9005c54..7e52b16911 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1097,10 +1097,13 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') max: 2 ); + // Is the role change being requested by the user on their own membership? + $isCurrentUserAnOwner = $user->getInternalId() === $membership->getAttribute('userInternalId'); + // Prevent role change if there's only one owner left, - // the requester is that owner, and the new `$roles` no longer include 'owner'! - if ($ownersCount === 1 && $isOwner && !\in_array('owner', $roles)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'There must be at least one owner in the organization.'); + // the requester is that owner, and the new `$roles` no longer include 'owner' + if ($ownersCount === 1 && $isOwner && $isCurrentUserAnOwner && !\in_array('owner', $roles)) { + throw new Exception(Exception::MEMBERSHIP_DOWNGRADE_PROHIBITED, 'There must be at least one owner in the organization.'); } } @@ -1315,10 +1318,12 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') )) ->param('teamId', '', new UID(), 'Team ID.') ->param('membershipId', '', new UID(), 'Membership ID.') + ->inject('user') + ->inject('project') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $queueForEvents) { + ->action(function (string $teamId, string $membershipId, Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents) { $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1326,9 +1331,9 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::TEAM_INVITE_NOT_FOUND); } - $user = $dbForProject->getDocument('users', $membership->getAttribute('userId')); + $profile = $dbForProject->getDocument('users', $membership->getAttribute('userId')); - if ($user->isEmpty()) { + if ($profile->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -1342,6 +1347,29 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH); } + if ($project->getId() === 'console') { + // Quick check: + // fetch up to 2 owners to determine if only one exists + $ownersCount = $dbForProject->count( + collection: 'memberships', + queries: [ + Query::contains('roles', ['owner']), + Query::equal('teamInternalId', [$team->getInternalId()]) + ], + max: 2 + ); + + // Is the deletion being requested by the user on their own membership and they are also the owner? + $isSelfOwner = + in_array('owner', $membership->getAttribute('roles')) && + $membership->getAttribute('userInternalId') === $user->getInternalId(); + + if ($ownersCount === 1 && $isSelfOwner) { + /* Prevent removal if the user is the only owner. */ + throw new Exception(Exception::MEMBERSHIP_DELETION_PROHIBITED, 'There must be at least one owner in the organization.'); + } + } + try { $dbForProject->deleteDocument('memberships', $membership->getId()); } catch (AuthorizationException $exception) { @@ -1350,15 +1378,15 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership from DB'); } - $dbForProject->purgeCachedDocument('users', $user->getId()); + $dbForProject->purgeCachedDocument('users', $profile->getId()); if ($membership->getAttribute('confirm')) { // Count only confirmed members Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0)); } $queueForEvents - ->setParam('userId', $user->getId()) ->setParam('teamId', $team->getId()) + ->setParam('userId', $profile->getId()) ->setParam('membershipId', $membership->getId()) ->setPayload($response->output($membership, Response::MODEL_MEMBERSHIP)) ; diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 588ca417cf..c8a0a36a85 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -1215,6 +1215,7 @@ App::post('/v1/vcs/github/events') if ($event == $github::EVENT_PUSH) { $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; + $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; $providerBranch = $parsedPayload["branch"] ?? ''; $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; $providerRepositoryId = $parsedPayload["repositoryId"] ?? ''; @@ -1236,8 +1237,8 @@ App::post('/v1/vcs/github/events') Query::limit(100), ])); - // create new deployment only on push and not when branch is created - if (!$providerBranchCreated) { + // create new deployment only on push and not when branch is created or deleted + if (!$providerBranchCreated && !$providerBranchDeleted) { $createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request); } } elseif ($event == $github::EVENT_INSTALLATION) { diff --git a/app/controllers/general.php b/app/controllers/general.php index bff701792e..5beac9e1e8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -749,12 +749,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw return false; } elseif ($type === 'redirect') { $url = $rule->getAttribute('redirectUrl', ''); - - $query = ($swooleRequest->server['query_string'] ?? ''); - if (!empty($query)) { - $url .= '?' . $query; - } - $response->redirect($url, \intval($rule->getAttribute('redirectStatusCode', 301))); return true; } else { diff --git a/app/views/general/404.phtml b/app/views/general/404.phtml index 5e63344c8a..2a9ff7f312 100644 --- a/app/views/general/404.phtml +++ b/app/views/general/404.phtml @@ -151,37 +151,6 @@
- -Powered by
- - - -