diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 6786757ffd..684817e37c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -19,7 +19,6 @@ use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; -use Cron\CronExpression; use PHPMailer\PHPMailer\PHPMailer; use Utopia\App; use Utopia\Audit\Audit; @@ -358,9 +357,18 @@ App::get('/v1/projects') throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); } + if(!empty($cursorDocument->getAttribute('_deletedAt')) && $cursorDocument->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); + } + $cursor->setValue($cursorDocument); } + $queries[] = query::or([ + query::isNull('_deletedAt'), + query::lessThan('_deletedAt', DateTime::now()) + ]); + $filterQueries = Query::groupByType($queries)['filters']; try { $projects = $dbForPlatform->find('projects', $queries); @@ -402,6 +410,10 @@ App::get('/v1/projects/:projectId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -435,10 +447,9 @@ App::patch('/v1/projects/:projectId') ->param('legalCity', '', new Text(256), 'Project legal city. Max length: 256 chars.', true) ->param('legalAddress', '', new Text(256), 'Project legal address. Max length: 256 chars.', true) ->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true) - ->param('restore', null, new Boolean(), 'Restore project from being marked for deletion', true) ->inject('response') ->inject('dbForPlatform') - ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, bool $restore, Response $response, Database $dbForPlatform) { + ->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); @@ -446,7 +457,12 @@ App::patch('/v1/projects/:projectId') throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + + $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project ->setAttribute('name', $name) ->setAttribute('description', $description) ->setAttribute('logo', $logo) @@ -457,14 +473,42 @@ App::patch('/v1/projects/:projectId') ->setAttribute('legalCity', $legalCity) ->setAttribute('legalAddress', $legalAddress) ->setAttribute('legalTaxId', $legalTaxId) - ->setAttribute('search', implode(' ', [$projectId, $name])) - ; + ->setAttribute('search', implode(' ', [$projectId, $name]))); - if (!empty($restore)) { - $project->setAttribute('_deletedAt', null); + $response->dynamic($project, Response::MODEL_PROJECT); + }); + +App::patch('/v1/projects/:projectId/restore') + ->desc('Restore project from being a candidate for deletion') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'projects', + name: 'restore', + description: '/docs/references/projects/restore.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROJECT, + ) + ] + )) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->inject('response') + ->inject('dbForPlatform') + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), + $project->setAttribute('_deletedAt', null) + ); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -499,6 +543,10 @@ App::patch('/v1/projects/:projectId/team') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + if ($team->isEmpty()) { throw new Exception(Exception::TEAM_NOT_FOUND); } @@ -574,6 +622,10 @@ App::patch('/v1/projects/:projectId/service') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $services = $project->getAttribute('services', []); $services[$service] = $status; @@ -611,6 +663,10 @@ App::patch('/v1/projects/:projectId/service/all') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])); $services = []; @@ -653,6 +709,10 @@ App::patch('/v1/projects/:projectId/api') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $apis = $project->getAttribute('apis', []); $apis[$api] = $status; @@ -690,6 +750,10 @@ App::patch('/v1/projects/:projectId/api/all') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $allApis = array_keys(Config::getParam('apis')); $apis = []; @@ -734,6 +798,10 @@ App::patch('/v1/projects/:projectId/oauth2') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $providers = $project->getAttribute('oAuthProviders', []); if ($appId !== null) { @@ -782,6 +850,10 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['sessionAlerts'] = $alerts; @@ -821,6 +893,10 @@ App::patch('/v1/projects/:projectId/auth/memberships-privacy') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['membershipsUserName'] = $userName; @@ -862,6 +938,10 @@ App::patch('/v1/projects/:projectId/auth/limit') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['limit'] = $limit; @@ -900,6 +980,10 @@ App::patch('/v1/projects/:projectId/auth/duration') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['duration'] = $duration; @@ -942,6 +1026,10 @@ App::patch('/v1/projects/:projectId/auth/:method') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths[$authKey] = $status; @@ -979,6 +1067,10 @@ App::patch('/v1/projects/:projectId/auth/password-history') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['passwordHistory'] = $limit; @@ -1017,6 +1109,10 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['passwordDictionary'] = $enabled; @@ -1055,6 +1151,10 @@ App::patch('/v1/projects/:projectId/auth/personal-data') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['personalDataCheck'] = $enabled; @@ -1093,6 +1193,10 @@ App::patch('/v1/projects/:projectId/auth/max-sessions') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['maxSessions'] = $limit; @@ -1139,6 +1243,10 @@ App::patch('/v1/projects/:projectId/auth/mock-numbers') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $auths = $project->getAttribute('auths', []); $auths['mockNumbers'] = $numbers; @@ -1170,16 +1278,19 @@ App::delete('/v1/projects/:projectId') )) ->param('projectId', '', new UID(), 'Project unique ID.') ->inject('response') - ->inject('user') ->inject('dbForPlatform') ->inject('queueForDeletes') - ->action(function (string $projectId, Response $response, Document $user, Database $dbForPlatform, Delete $queueForDeletes) { + ->action(function (string $projectId, Response $response, Database $dbForPlatform) { $project = $dbForPlatform->getDocument('projects', $projectId); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $expiration = System::getEnv('_APP_PROJECTS_DELETE_EXPIRATION','3600'); $deletedAt = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), (int)$expiration)); $dbForPlatform->updateDocument('projects', $project->getId(), @@ -1226,6 +1337,10 @@ App::post('/v1/projects/:projectId/webhooks') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); $webhook = new Document([ @@ -1284,6 +1399,10 @@ App::get('/v1/projects/:projectId/webhooks') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $webhooks = $dbForPlatform->find('webhooks', [ Query::equal('projectInternalId', [$project->getInternalId()]), Query::limit(5000), @@ -1324,6 +1443,10 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1372,6 +1495,10 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); $webhook = $dbForPlatform->findOne('webhooks', [ @@ -1431,6 +1558,10 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1478,6 +1609,10 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $webhook = $dbForPlatform->findOne('webhooks', [ Query::equal('$id', [$webhookId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1527,6 +1662,10 @@ App::post('/v1/projects/:projectId/keys') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $key = new Document([ '$id' => ID::unique(), '$permissions' => [ @@ -1581,6 +1720,10 @@ App::get('/v1/projects/:projectId/keys') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $keys = $dbForPlatform->find('keys', [ Query::equal('projectInternalId', [$project->getInternalId()]), Query::limit(5000), @@ -1621,6 +1764,10 @@ App::get('/v1/projects/:projectId/keys/:keyId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1665,6 +1812,10 @@ App::put('/v1/projects/:projectId/keys/:keyId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1716,6 +1867,10 @@ App::delete('/v1/projects/:projectId/keys/:keyId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $key = $dbForPlatform->findOne('keys', [ Query::equal('$id', [$keyId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1764,6 +1919,10 @@ App::post('/v1/projects/:projectId/jwts') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); $response @@ -1810,6 +1969,10 @@ App::post('/v1/projects/:projectId/platforms') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $platform = new Document([ '$id' => ID::unique(), '$permissions' => [ @@ -1903,6 +2066,10 @@ App::get('/v1/projects/:projectId/platforms/:platformId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -1947,6 +2114,10 @@ App::put('/v1/projects/:projectId/platforms/:platformId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -2001,6 +2172,10 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $platform = $dbForPlatform->findOne('platforms', [ Query::equal('$id', [$platformId]), Query::equal('projectInternalId', [$project->getInternalId()]), @@ -2056,6 +2231,10 @@ App::patch('/v1/projects/:projectId/smtp') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + // Ensure required params for when enabling SMTP if ($enabled) { if (empty($senderName)) { @@ -2154,6 +2333,10 @@ App::post('/v1/projects/:projectId/smtp/tests') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $replyToEmail = !empty($replyTo) ? $replyTo : $senderEmail; $subject = 'Custom SMTP email sample'; @@ -2223,6 +2406,10 @@ App::get('/v1/projects/:projectId/templates/sms/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $template = $templates['sms.' . $type . '-' . $locale] ?? null; @@ -2269,6 +2456,10 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $template = $templates['email.' . $type . '-' . $locale] ?? null; @@ -2331,6 +2522,10 @@ App::patch('/v1/projects/:projectId/templates/sms/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $templates['sms.' . $type . '-' . $locale] = [ 'message' => $message @@ -2380,6 +2575,10 @@ App::patch('/v1/projects/:projectId/templates/email/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $templates['email.' . $type . '-' . $locale] = [ 'senderName' => $senderName, @@ -2435,6 +2634,10 @@ App::delete('/v1/projects/:projectId/templates/sms/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $template = $templates['sms.' . $type . '-' . $locale] ?? null; @@ -2484,6 +2687,10 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale') throw new Exception(Exception::PROJECT_NOT_FOUND); } + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + $templates = $project->getAttribute('templates', []); $template = $templates['email.' . $type . '-' . $locale] ?? null; diff --git a/app/init/resources.php b/app/init/resources.php index 6ca0522b6c..42a2ab254f 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -31,6 +31,7 @@ use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; +use Utopia\Database\DateTime; use Utopia\Database\DateTime as DatabaseDateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; @@ -294,6 +295,10 @@ App::setResource('project', function ($dbForPlatform, $request, $console) { $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); + if(!empty($project->getAttribute('_deletedAt')) && $project->getAttribute('_deletedAt') < DateTime::now()) { + $project = new Document([]); + } + return $project; }, ['dbForPlatform', 'request', 'console']); diff --git a/docker-compose.yml b/docker-compose.yml index e262803002..29a43aca91 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -932,34 +932,6 @@ services: - _APP_DB_PASS - _APP_DATABASE_SHARED_TABLES - appwrite-task-scheduler-projects: - entrypoint: schedule-projects - <<: *x-logging - container_name: appwrite-task-scheduler-projects - image: appwrite-dev - networks: - - appwrite - volumes: - - ./app:/usr/src/code/app - - ./src:/usr/src/code/src - depends_on: - - mariadb - - redis - environment: - - _APP_ENV - - _APP_WORKER_PER_CORE - - _APP_OPENSSL_KEY_V1 - - _APP_REDIS_HOST - - _APP_REDIS_PORT - - _APP_REDIS_USER - - _APP_REDIS_PASS - - _APP_DB_HOST - - _APP_DB_PORT - - _APP_DB_SCHEMA - - _APP_DB_USER - - _APP_DB_PASS - - _APP_DATABASE_SHARED_TABLES - appwrite-assistant: container_name: appwrite-assistant image: appwrite/assistant:0.7.0 diff --git a/docs/references/projects/restore.md b/docs/references/projects/restore.md new file mode 100644 index 0000000000..1500f9086d --- /dev/null +++ b/docs/references/projects/restore.md @@ -0,0 +1 @@ +Restore project from being a candidate for deletion. \ No newline at end of file diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php index fd9d05dec9..157434a9ab 100644 --- a/src/Appwrite/Platform/Tasks/Maintenance.php +++ b/src/Appwrite/Platform/Tasks/Maintenance.php @@ -9,6 +9,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\System\System; @@ -87,7 +88,26 @@ class Maintenance extends Action $this->renewCertificates($dbForPlatform, $queueForCertificates); $this->notifyDeleteCache($cacheRetention, $queueForDeletes); $this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes); - }, $interval, $delay); + Console::warning("Deleting projects that there delete date smaller then ".DateTime::now()); + $dbForPlatform->foreach( + 'projects', + function (Document $project) use ($dbForPlatform, $queueForDeletes) { + + $queueForDeletes + ->setProject($project) + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($project) + ->trigger(); + + Authorization::skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId())); + }, [ + Query::lessThan('_deletedAt', DateTime::now()), + Query::equal('region', [System::getEnv('_APP_REGION', 'default')]), + ] + ); + + + }, $interval, 0); } private function notifyDeleteConnections(Delete $queueForDeletes): void diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index aea1971be7..575e115374 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3876,7 +3876,25 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(404, $project['headers']['status-code']); + var_dump(['before restore' => $response['headers']['status-code']]); + // restore from deletion + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/restore', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); + + var_dump(['restore' => $response['headers']['status-code']]); + + $project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $project['headers']['status-code']); + var_dump(['after restore' => $response['headers']['status-code']]); return $data; }