Add unique url generator
This commit is contained in:
parent
0cf6a50c3c
commit
068acef0f6
8 changed files with 220 additions and 103 deletions
4
.env
4
.env
|
@ -13,10 +13,10 @@ _APP_SYSTEM_RESPONSE_FORMAT=
|
|||
_APP_OPTIONS_ABUSE=disabled
|
||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
_APP_DOMAIN=demo.appwrite.io
|
||||
_APP_DOMAIN=localhost
|
||||
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
||||
_APP_DOMAIN_DO_TOKEN=
|
||||
_APP_DOMAIN_TARGET=demo.appwrite.io
|
||||
_APP_DOMAIN_TARGET=localhost
|
||||
_APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
|
|
|
@ -769,6 +769,96 @@ $collections = [
|
|||
],
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('routes'),
|
||||
'name' => 'Routes',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('projectId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('projectInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('domain'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('resourceType'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 100,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('resourceInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('resourceId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_domain'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['domain'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_resource_id'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resourceInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'schedules' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('schedules'),
|
||||
|
|
|
@ -514,14 +514,14 @@ return [
|
|||
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::ROUTER_INVALID_URL => [
|
||||
'name' => Exception::ROUTER_INVALID_URL,
|
||||
'description' => 'Invalid preview URL for Appwrite Router.',
|
||||
Exception::ROUTER_INVALID_TYPE => [
|
||||
'name' => Exception::ROUTER_INVALID_TYPE,
|
||||
'description' => 'Invalid domain configuration. Route type is not supported.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::ROUTER_CONSOLE_PROJECT => [
|
||||
'name' => Exception::ROUTER_CONSOLE_PROJECT,
|
||||
'description' => 'Use of "console" project with Appwrite Router is not allowed.',
|
||||
Exception::ROUTER_UNKNOWN_HOST => [
|
||||
'name' => Exception::ROUTER_UNKNOWN_HOST,
|
||||
'description' => 'Host is not trusted. Add a custom domain to your project first.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::WEBHOOK_NOT_FOUND => [
|
||||
|
|
|
@ -38,7 +38,6 @@ use Utopia\Validator\Range;
|
|||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Config\Config;
|
||||
use Executor\Executor;
|
||||
use Selective\Base32\Base32;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
@ -105,6 +104,19 @@ App::post('/v1/functions')
|
|||
]))
|
||||
);
|
||||
|
||||
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
$routeSubdomain = ID::unique();
|
||||
$route = Authorization::skip(
|
||||
fn() => $dbForConsole->createDocument('routes', new Document([
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => "{$routeSubdomain}.{$functionsDomain}",
|
||||
'resourceType' => 'function',
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
]))
|
||||
);
|
||||
|
||||
$function->setAttribute('scheduleId', $schedule->getId());
|
||||
$function->setAttribute('scheduleInternalId', $schedule->getInternalId());
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
|
@ -132,7 +144,8 @@ App::get('/v1/functions')
|
|||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Document $project, Response $response, Database $dbForProject) {
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $queries, string $search, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
|
@ -157,15 +170,18 @@ App::get('/v1/functions')
|
|||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
$projectId = $project->getId();
|
||||
$base32 = new Base32();
|
||||
$projectIdHash = \rtrim($base32->encode($projectId), '=');
|
||||
$functions = $dbForProject->find('functions', $queries);
|
||||
$functions = \array_map(function (Document $function) use ($functionsDomain, $projectIdHash, $base32) {
|
||||
$functionId = $function->getId();
|
||||
$functionIdHash = \rtrim($base32->encode($functionId), '=');
|
||||
$function = $function->setAttribute('url', "{$functionIdHash}-{$projectIdHash}.{$functionsDomain}");
|
||||
$functions = \array_map(function (Document $function) use ($dbForConsole, $project) {
|
||||
$route = Authorization::skip(
|
||||
fn() => $dbForConsole->find('routes', [
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceInternalId', [ $function->getInternalId() ]),
|
||||
Query::equal('projectInternalId', [ $project->getInternalId() ]),
|
||||
Query::limit(1),
|
||||
Query::orderAsc('_createdAt')
|
||||
])
|
||||
)[0] ?? null;
|
||||
$function = $function->setAttribute('url', $route ? $route->getAttribute('domain') : '');
|
||||
return $function;
|
||||
}, $functions);
|
||||
|
||||
|
@ -217,21 +233,24 @@ App::get('/v1/functions/:functionId')
|
|||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $functionId, Document $project, Response $response, Database $dbForProject) {
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $functionId, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
$projectId = $project->getId();
|
||||
$functionId = $function->getId();
|
||||
$base32 = new Base32();
|
||||
$projectIdHash = \rtrim($base32->encode($projectId), '=');
|
||||
$functionIdHash = \rtrim($base32->encode($functionId), '=');
|
||||
|
||||
$function = $function->setAttribute('url', "{$functionIdHash}-{$projectIdHash}.{$functionsDomain}");
|
||||
$route = Authorization::skip(
|
||||
fn() => $dbForConsole->find('routes', [
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceInternalId', [ $function->getInternalId() ]),
|
||||
Query::equal('projectInternalId', [ $project->getInternalId() ]),
|
||||
Query::limit(1),
|
||||
Query::orderAsc('_createdAt')
|
||||
])
|
||||
)[0] ?? null;
|
||||
$function = $function->setAttribute('url', $route ? $route->getAttribute('domain') : '');
|
||||
|
||||
$response->dynamic($function, Response::MODEL_FUNCTION);
|
||||
});
|
||||
|
|
|
@ -37,7 +37,6 @@ use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
|
|||
use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
|
||||
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
|
||||
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
||||
use Selective\Base32\Base32;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
|
@ -65,83 +64,82 @@ App::init()
|
|||
* Appwrite Router
|
||||
*/
|
||||
$host = $swooleRequest->header['host'] ?? '';
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
|
||||
// Function Preview
|
||||
if (\str_ends_with($host, App::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
|
||||
// Remove domain from host
|
||||
$previewDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
$host = \substr($host, 0, -1 * (1 + \strlen($previewDomain))); // +1 for starting dot (.)
|
||||
// Only run Router when external domain
|
||||
if($host !== $mainDomain) {
|
||||
$route = Authorization::skip(
|
||||
fn() => $dbForConsole->find('routes', [
|
||||
Query::equal('domain', [$host]),
|
||||
Query::limit(1)
|
||||
])
|
||||
)[0] ?? null;
|
||||
|
||||
if (empty($host) || \str_contains($host, '.')) {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_INVALID_URL);
|
||||
if($route === null) {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_UNKNOWN_HOST);
|
||||
}
|
||||
|
||||
$type = $route->getAttribute('resourceType');
|
||||
|
||||
$ids = explode('-', $host);
|
||||
if($type === 'function') {
|
||||
$functionId = $route->getAttribute('resourceId');
|
||||
$projectId = $route->getAttribute('projectId');
|
||||
|
||||
if (\count($ids) !== 2) {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_INVALID_URL);
|
||||
$body = \json_encode([
|
||||
'async' => false,
|
||||
'body' => $swooleRequest->getContent() ?? '',
|
||||
'method' => $swooleRequest->server['request_method'],
|
||||
'path' => $swooleRequest->server['path_info'],
|
||||
'headers' => $swooleRequest->header
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'X-Appwrite-Project: ' . $projectId
|
||||
];
|
||||
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// \curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
|
||||
$executionResponse = \curl_exec($ch);
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = \curl_error($ch);
|
||||
$errNo = \curl_errno($ch);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
return $response->setStatusCode(500)->send("Internal error: " . $error);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$error = \json_decode($executionResponse, true)['message'];
|
||||
return $response->setStatusCode(500)->send("Execution error: " . $error);
|
||||
}
|
||||
|
||||
$execution = \json_decode($executionResponse, true);
|
||||
|
||||
foreach ($execution['headers'] as $header => $value) {
|
||||
$response->setHeader($header, $value);
|
||||
}
|
||||
|
||||
$body = $execution['body'] ?? '';
|
||||
|
||||
if (($execution['headers']['x-open-runtimes-encoding'] ?? '') === 'base64') {
|
||||
$body = \base64_decode($body);
|
||||
}
|
||||
|
||||
return $response->setStatusCode($execution['statusCode'] ?? 200)->send($body);
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_INVALID_TYPE);
|
||||
}
|
||||
|
||||
// Active deployment preview
|
||||
$functionIdHash = $ids[0];
|
||||
$projectIdHash = $ids[1];
|
||||
|
||||
$base32 = new Base32();
|
||||
$functionId = $base32->decode($functionIdHash);
|
||||
$projectId = $base32->decode($projectIdHash);
|
||||
|
||||
$body = \json_encode([
|
||||
'async' => false,
|
||||
'body' => $swooleRequest->getContent() ?? '',
|
||||
'method' => $swooleRequest->server['request_method'],
|
||||
'path' => $swooleRequest->server['path_info'],
|
||||
'headers' => $swooleRequest->header
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'X-Appwrite-Project: ' . $projectId
|
||||
];
|
||||
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// \curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
|
||||
$executionResponse = \curl_exec($ch);
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = \curl_error($ch);
|
||||
$errNo = \curl_errno($ch);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
return $response->setStatusCode(500)->send("Internal error: " . $error);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$error = \json_decode($executionResponse, true)['message'];
|
||||
return $response->setStatusCode(500)->send("Execution error: " . $error);
|
||||
}
|
||||
|
||||
$execution = \json_decode($executionResponse, true);
|
||||
|
||||
foreach ($execution['headers'] as $header => $value) {
|
||||
$response->setHeader($header, $value);
|
||||
}
|
||||
|
||||
$body = $execution['body'] ?? '';
|
||||
|
||||
if (($execution['headers']['x-open-runtimes-encoding'] ?? '') === 'base64') {
|
||||
$body = \base64_decode($body);
|
||||
}
|
||||
|
||||
return $response->setStatusCode($execution['statusCode'] ?? 200)->send($body);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -450,8 +450,19 @@ class DeletesV1 extends Worker
|
|||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$functionId = $document->getId();
|
||||
|
||||
/**
|
||||
* Delete routes
|
||||
*/
|
||||
Console::info("Deleting routes for function " . $functionId);
|
||||
$this->deleteByGroup('routes', [
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceInternalId', [$document->getInternalId()]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()])
|
||||
], $dbForConsole);
|
||||
|
||||
/**
|
||||
* Delete Variables
|
||||
*/
|
||||
|
|
|
@ -73,8 +73,7 @@
|
|||
"phpmailer/phpmailer": "6.6.0",
|
||||
"chillerlan/php-qrcode": "4.3.3",
|
||||
"adhocore/jwt": "1.1.2",
|
||||
"slickdeals/statsd": "3.1.0",
|
||||
"selective/base32": "^2.0"
|
||||
"slickdeals/statsd": "3.1.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
|
|
@ -164,8 +164,8 @@ class Exception extends \Exception
|
|||
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
||||
|
||||
/** Router */
|
||||
public const ROUTER_INVALID_URL = 'router_invalid_url';
|
||||
public const ROUTER_CONSOLE_PROJECT = 'router_console_project';
|
||||
public const ROUTER_UNKNOWN_HOST = 'router_unknown_host';
|
||||
public const ROUTER_INVALID_TYPE = 'router_unknown_type';
|
||||
|
||||
/** Keys */
|
||||
public const KEY_NOT_FOUND = 'key_not_found';
|
||||
|
|
Loading…
Reference in a new issue