From 2224ea7cea98520c55de73e7f1d1a16efd815d7f Mon Sep 17 00:00:00 2001 From: wess Date: Thu, 30 Mar 2023 11:58:20 -0400 Subject: [PATCH] Endpoints and updates for domain model --- app/config/collections.php | 12 + app/controllers/api/domains.php | 544 ++++++++++++++++-- app/init.php | 17 + composer.json | 2 +- composer.lock | 36 +- src/Appwrite/Utopia/Response/Model/Domain.php | 6 + 6 files changed, 557 insertions(+), 60 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index ab6d665296..c2daf999d6 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -945,6 +945,18 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('registered'), + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], 'indexes' => [ [ diff --git a/app/controllers/api/domains.php b/app/controllers/api/domains.php index ff9d734f4a..43f34569a5 100644 --- a/app/controllers/api/domains.php +++ b/app/controllers/api/domains.php @@ -1,88 +1,544 @@ groups(['domains']) - ->inject('domain') - ->action(function (Document $domain) { -}); + ->groups(['projects']) + ->inject('project') + ->action(function (Document $project) { + if ($project->getId() !== 'console') { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN); + } + }); + +App::post('/v1/domains/suggest') + ->desc("Suggest domain names") + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('domain', null, new DomainValidator(), 'Domain name') + ->inject('response') + ->inject('registrar') + ->action(function (string $domain, $response, $registrar) { + + $suggestions = $registrar->suggest($domain); + $domains = []; + + foreach($suggestions as $k=>$_v) { + $domains[] = new Domain($k); + } + + $response->dynamic(["domains" => $domains], Response::MODEL_DOMAIN_LIST); + }); + +App::post('/v1/domains/available') + ->desc("Checks if a domain is available for registration") + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('domain', null, new DomainValidator(), 'Domain name') + ->inject('response') + ->inject('registrar') + ->action(function (string $domain, $response, $registrar) { + + $domain = [ + "domain" => new Domain($domain), + "available" => $registrar->available($domain) + ]; + + $response->dynamic(["domain" => $domain], Response::MODEL_DOMAIN); + }); App::post('/v1/domains') - ->action(function () { + ->desc("Create a domain for 3rd party registrar, prompt to assign nameservers") + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->param('domain', null, new DomainValidator(), 'Domain name') + ->inject('response') + ->inject('dbForConsole') + ->inject('registrar') + ->action(function ( + string $projectId, + string $domain, + Response $response, + Database $dbForConsole, + Registrar $registrar + ) { -}); + if($registrar->available($domain)) { + throw new Exception(); + } + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $document = $dbForConsole->findOne('domains', [ + Query::equal('domain', [$domain]), + Query::equal('projectInternalId', [$project->getInternalId()]), + ]); + + if ($document && !$document->isEmpty()) { + throw new Exception(Exception::DOMAIN_ALREADY_EXISTS); + } + + $domain = new Domain($domain); + + $domain = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'projectInternalId' => $project->getInternalId(), + 'projectId' => $project->getId(), + 'domain' => $domain->get(), + 'tld' => $domain->getSuffix(), + 'registerable' => $domain->getRegisterable(), + 'verification' => false, + 'certificateId' => null, + 'registered' => false, + ]); + + $domain = $dbForConsole->createDocument('domains', $domain); + + $dbForConsole->deleteCachedDocument('projects', $project->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($domain, Response::MODEL_DOMAIN); + }); App::post('/v1/domains/purchase') - ->action(function () { + ->desc("Purchase, create and domain assign nameservers") + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->param('domain', null, new DomainValidator(), 'Domain name') + ->param('firstname', '', new Text(128), 'First name') + ->param('lastname', '', new Text(128), 'Last name') + ->param('phone', '', new Phone(), 'Phone number') + ->param('email', '', new Email(), 'Email address') + ->param('address1', '', new Text(128), 'Address') + ->param('address2', '', new Text(128), 'Address 2') + ->param('address3', '', new Text(128), 'Address 3') + ->param('city', '', new Text(128), 'City') + ->param('state', '', new Text(128), 'State') + ->param('country', '', new Text(128), 'Country') + ->param('postalcode', '', new Text(128), 'Postal code') + ->param('org', '', new Text(128), 'Organization') + ->inject('response') + ->inject('dbForConsole') + ->inject('registrar') + ->action(function ( + string $projectId, + string $domain, + string $firstname, + string $lastname, + string $phone, + string $email, + string $address1, + string $address2, + string $address3, + string $city, + string $state, + string $country, + string $postalcode, + string $org, + Response $response, + Database $dbForConsole, + Registrar $registrar + ) { -}); + if(!$registrar->available($domain)) { + throw new Exception(); + } + + + $contact = new Contact( + $firstname, + $lastname, + $phone, + $email, + $address1, + $address2, + $address3, + $city, + $state, + $country, + $postalcode, + $org, + '' + ); + + try { + + $registrar->purchase($domain, [$contact]); + + } catch (Exception $e) { + + throw new Exception(); + + } + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $document = $dbForConsole->findOne('domains', [ + Query::equal('domain', [$domain]), + Query::equal('projectInternalId', [$project->getInternalId()]), + ]); + + if ($document && !$document->isEmpty()) { + throw new Exception(Exception::DOMAIN_ALREADY_EXISTS); + } + + $domain = new Domain($domain); + + $domain = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'projectInternalId' => $project->getInternalId(), + 'projectId' => $project->getId(), + 'domain' => $domain->get(), + 'tld' => $domain->getSuffix(), + 'registerable' => $domain->getRegisterable(), + 'verification' => false, + 'certificateId' => null, + 'registered' => true, + ]); + + $domain = $dbForConsole->createDocument('domains', $domain); + + $dbForConsole->deleteCachedDocument('projects', $project->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($domain, Response::MODEL_DOMAIN); + + }); App::post('/v1/domains/transfer/in') - ->action(function () { + ->desc("Transfer existing domain to Appwrite, and assign nameservers") + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->param('domain', null, new DomainValidator(), 'Domain name') + ->param('firstname', '', new Text(128), 'First name') + ->param('lastname', '', new Text(128), 'Last name') + ->param('phone', '', new Phone(), 'Phone number') + ->param('email', '', new Email(), 'Email address') + ->param('address1', '', new Text(128), 'Address') + ->param('address2', '', new Text(128), 'Address 2') + ->param('address3', '', new Text(128), 'Address 3') + ->param('city', '', new Text(128), 'City') + ->param('state', '', new Text(128), 'State') + ->param('country', '', new Text(128), 'Country') + ->param('postalcode', '', new Text(128), 'Postal code') + ->param('org', '', new Text(128), 'Organization') + ->inject('response') + ->inject('dbForConsole') + ->inject('registrar') + ->action(function ( + string $projectId, + string $domain, + string $firstname, + string $lastname, + string $phone, + string $email, + string $address1, + string $address2, + string $address3, + string $city, + string $state, + string $country, + string $postalcode, + string $org, + Response $response, + Database $dbForConsole, + Registrar $registrar + ) { -}); + if(!$registrar->available($domain)) { + throw new Exception(); + } + + + $contact = new Contact( + $firstname, + $lastname, + $phone, + $email, + $address1, + $address2, + $address3, + $city, + $state, + $country, + $postalcode, + $org, + '' + ); + + try { + + $registrar->transfer($domain, [$contact]); + + } catch (Exception $e) { + + throw new Exception(); + + } + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $document = $dbForConsole->findOne('domains', [ + Query::equal('domain', [$domain]), + Query::equal('projectInternalId', [$project->getInternalId()]), + ]); + + if ($document && !$document->isEmpty()) { + throw new Exception(Exception::DOMAIN_ALREADY_EXISTS); + } + + $domain = new Domain($domain); + + $domain = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'projectInternalId' => $project->getInternalId(), + 'projectId' => $project->getId(), + 'domain' => $domain->get(), + 'tld' => $domain->getSuffix(), + 'registerable' => $domain->getRegisterable(), + 'verification' => false, + 'certificateId' => null, + 'registered' => true, + ]); + + $domain = $dbForConsole->createDocument('domains', $domain); + + $dbForConsole->deleteCachedDocument('projects', $project->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($domain, Response::MODEL_DOMAIN); + + }); App::post('/v1/domains/transfer/out') + ->desc("Start transfer process for domain and generate code for transfer to 3rd party registrar") + ->groups(['api', 'domains']) + ->label('scope', 'domains.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'transferOut') + ->label('sdk.method', 'create') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->inject('response') + ->inject('user') ->action(function () { - -}); + ///TODO + }); App::get('/v1/domains') - ->action(function () { + ->desc("List domains") + ->groups(['api', 'domains']) + ->label('scope', 'domains.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'list') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, Response $response, Database $dbForConsole) { + $project = $dbForConsole->getDocument('projects', $projectId); -}); + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $domains = $dbForConsole->find('domains', [ + Query::equal('projectInternalId', [$project->getInternalId()]), + Query::limit(5000), + ]); + + $response->dynamic(new Document([ + 'domains' => $domains, + 'total' => count($domains), + ]), Response::MODEL_DOMAIN_LIST); + + }); App::get('/v1/domains/:domainId') - ->action(function () { + ->desc("Get domain") + ->groups(['api', 'domains']) + ->label('scope', 'domains.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'list') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->param('domainId', '', new UID(), 'Domain unique ID') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) { -}); + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $domain = $dbForConsole->findOne('domains', [ + Query::equal('_uid', [$domainId]), + Query::equal('projectInternalId', [$project->getInternalId()]), + ]); + + if ($domain === false || $domain->isEmpty()) { + throw new Exception(Exception::DOMAIN_NOT_FOUND); + } + + $response->dynamic($domain, Response::MODEL_DOMAIN); + }); App::patch('/v1/domains/:domainId/nameservers') + ->desc("Check namserver records and update them if needed") + ->groups(['api', 'domains']) + ->label('scope', 'domains.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'updateNameservers') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('domainId', '', new UID(), 'Domain unique ID') + ->inject('response') + ->inject('user') ->action(function () { - -}); + ///Do we need/want? + }); App::patch('/v1/domains/:domainId/project') + ->desc("Move domain to a different project") + ->groups(['api', 'domains']) + ->label('scope', 'domains.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'updateProject') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('domainId', '', new UID(), 'Domain unique ID') + ->inject('response') + ->inject('user') ->action(function () { - -}); + /// WAIT FOR TRANSFER SERVICE. + }); App::delete('/v1/domains/:domainId') - ->action(function () { + ->desc("Remove a domain") + ->groups(['api', 'domains']) + ->label('scope', 'domains.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'domains') + ->label('sdk.method', 'list') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_DOMAIN) + ->param('projectId', '', new UID(), 'Project unique ID') + ->param('domainId', '', new UID(), 'Domain unique ID') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) { -}); \ No newline at end of file + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $domain = $dbForConsole->findOne('domains', [ + Query::equal('_uid', [$domainId]), + Query::equal('projectInternalId', [$project->getInternalId()]), + ]); + + if ($domain === false || $domain->isEmpty()) { + throw new Exception(Exception::DOMAIN_NOT_FOUND); + } + + if($domain['registered'] === true) { + throw new Exception(); + } + + $response->noContent(); + }); diff --git a/app/init.php b/app/init.php index d12510dc29..abf79ba173 100644 --- a/app/init.php +++ b/app/init.php @@ -55,6 +55,8 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\DatetimeValidator; use Utopia\Database\Validator\Structure; +use Utopia\Domains\Registrar; +use Utopia\Domains\Registrar\OpenSRS; use Utopia\Locale\Locale; use Utopia\Messaging\Adapters\SMS\Mock; use Utopia\Messaging\Adapters\SMS\Msg91; @@ -740,6 +742,21 @@ App::setResource('register', fn() => $register); App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en'))); +App::setResource('registrar', function(){ + $opensrs = new OpenSRS( + 'apikey', + 'apisecret', + 'username', + 'password', + [ + 'ns1.nameserver.com', + 'ns2.nameserver.com', + ] + ); + + return new Registrar($opensrs); +}); + // Queues App::setResource('events', fn() => new Event('', '')); App::setResource('audits', fn() => new Audit()); diff --git a/composer.json b/composer.json index 73e50a7572..7b5ec82348 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/config": "0.2.*", "utopia-php/database": "0.30.*", "utopia-php/preloader": "0.2.*", - "utopia-php/domains": "1.1.*", + "utopia-php/domains": "0.3.*", "utopia-php/framework": "0.26.*", "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", diff --git a/composer.lock b/composer.lock index bdbdb61572..157d1dc034 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3b088b0f09093a6f094f65f73b96bf91", + "content-hash": "6585f1a249e842abc698472ab942d9d8", "packages": [ { "name": "adhocore/jwt", @@ -2169,23 +2169,25 @@ }, { "name": "utopia-php/domains", - "version": "v1.1.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73" + "reference": "f7877b65c2c0e4219544bba42852ebad770e7939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/f7877b65c2c0e4219544bba42852ebad770e7939", + "reference": "f7877b65c2c0e4219544bba42852ebad770e7939", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0", + "utopia-php/framework": "0.*.*" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -2201,6 +2203,10 @@ { "name": "Eldad Fux", "email": "eldad@appwrite.io" + }, + { + "name": "Wess Cope", + "email": "wess@appwrite.io" } ], "description": "Utopia Domains library is simple and lite library for parsing web domains. This library is aiming to be as simple and easy to learn and use.", @@ -2217,9 +2223,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/master" + "source": "https://github.com/utopia-php/domains/tree/0.3.0" }, - "time": "2020-02-23T07:40:02+00:00" + "time": "2023-03-22T16:16:03+00:00" }, { "name": "utopia-php/framework", @@ -3660,16 +3666,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5" + "reference": "dfc078e8af9c99210337325ff5aa152872c98714" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5", - "reference": "1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dfc078e8af9c99210337325ff5aa152872c98714", + "reference": "dfc078e8af9c99210337325ff5aa152872c98714", "shasum": "" }, "require": { @@ -3712,9 +3718,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.1" }, - "time": "2023-03-12T10:13:29+00:00" + "time": "2023-03-27T19:02:04+00:00" }, { "name": "phpspec/prophecy", diff --git a/src/Appwrite/Utopia/Response/Model/Domain.php b/src/Appwrite/Utopia/Response/Model/Domain.php index 135bb52f90..28b8f593ba 100644 --- a/src/Appwrite/Utopia/Response/Model/Domain.php +++ b/src/Appwrite/Utopia/Response/Model/Domain.php @@ -63,6 +63,12 @@ class Domain extends Model 'default' => '', 'example' => '6ejea5c13377e', ]) + ->addRule('registered', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Registered with Appwrite', + 'default' => false, + 'example' => true, + ]) ; }