Merge branch '1.6.x' into feat-move-functions-marketplace-to-appwrite
This commit is contained in:
commit
12487e17b0
15 changed files with 426 additions and 18 deletions
|
@ -4751,8 +4751,8 @@ $consoleCollections = array_merge([
|
|||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
|
|
|
@ -2109,7 +2109,7 @@ App::post('/v1/users/:userId/jwts')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_JWT)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('sessionId', 'recent', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true)
|
||||
->param('sessionId', '', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true)
|
||||
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
|
@ -2137,17 +2137,13 @@ App::post('/v1/users/:userId/jwts')
|
|||
}
|
||||
}
|
||||
|
||||
if ($session->isEmpty()) {
|
||||
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document(['jwt' => $jwt->encode([
|
||||
'userId' => $user->getId(),
|
||||
'sessionId' => $session->getId()
|
||||
'sessionId' => $session->isEmpty() ? '' : $session->getId()
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
|
|
|
@ -11,9 +11,11 @@ use Appwrite\Network\Validator\Origin;
|
|||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
|
||||
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
|
@ -434,6 +436,9 @@ App::init()
|
|||
if (version_compare($requestFormat, '1.5.0', '<')) {
|
||||
$request->addFilter(new RequestV17());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.6.0', '<')) {
|
||||
$request->addFilter(new RequestV18());
|
||||
}
|
||||
}
|
||||
|
||||
$domain = $request->getHostname();
|
||||
|
@ -550,6 +555,9 @@ App::init()
|
|||
if (version_compare($responseFormat, '1.5.0', '<')) {
|
||||
$response->addFilter(new ResponseV17());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.6.0', '<')) {
|
||||
$response->addFilter(new ResponseV18());
|
||||
}
|
||||
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
|
||||
$response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks");
|
||||
}
|
||||
|
|
|
@ -206,6 +206,7 @@ App::init()
|
|||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
// Remove after migration
|
||||
if(!\str_contains($apiKey, '_')) {
|
||||
$keyType = API_KEY_STANDARD;
|
||||
$authKey = $apiKey;
|
||||
|
|
13
app/init.php
13
app/init.php
|
@ -118,7 +118,7 @@ const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
|
|||
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4314;
|
||||
const APP_VERSION_STABLE = '1.5.7';
|
||||
const APP_VERSION_STABLE = '1.6.0';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
@ -1243,14 +1243,15 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
|||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
|
||||
if ($jwtUserId && $jwtSessionId) {
|
||||
if (!empty($jwtUserId)) {
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
|
||||
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
|
||||
$user = new Document([]);
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if(!empty($jwtSessionId)) {
|
||||
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
|
||||
$user = new Document([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ $image = $this->getParam('image', '');
|
|||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: <?php echo $organization; ?>/console:appwrite/console:5.0.0-rc.5
|
||||
image: <?php echo $organization; ?>/console:appwrite/console:5.0.0-rc.11
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
|
|
@ -195,7 +195,7 @@ services:
|
|||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:5.0.0-rc.5
|
||||
image: appwrite/console:5.0.0-rc.11
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
|
|
@ -86,6 +86,7 @@ abstract class Migration
|
|||
'1.5.5' => 'V20',
|
||||
'1.5.6' => 'V20',
|
||||
'1.5.7' => 'V20',
|
||||
'1.6.0' => 'V21'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
153
src/Appwrite/Migration/Version/V21.php
Normal file
153
src/Appwrite/Migration/Version/V21.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class V21 extends Migration
|
||||
{
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
/**
|
||||
* Disable SubQueries for Performance.
|
||||
*/
|
||||
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
|
||||
Database::addFilter(
|
||||
$name,
|
||||
fn () => null,
|
||||
fn () => []
|
||||
);
|
||||
}
|
||||
|
||||
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
|
||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Collections.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception|Throwable
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
$internalProjectId = $this->project->getInternalId();
|
||||
$collectionType = match ($internalProjectId) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
$collections = $this->collections[$collectionType];
|
||||
foreach ($collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
$this->projectDB->setNamespace("_$internalProjectId");
|
||||
|
||||
switch ($id) {
|
||||
case 'projects':
|
||||
// Create accessedAt attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
case 'schedules':
|
||||
// Create data attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'data');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'data' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'functions':
|
||||
// Create scopes attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'scopes');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'scopes' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create size attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'size');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'size' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'executions':
|
||||
// Create requestMethod index
|
||||
try {
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_requestMethod');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_requestMethod' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create requestPath index
|
||||
try {
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_requestPath');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_requestPath' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create deployment index
|
||||
try {
|
||||
$this->createIndexFromCollection($this->projectDB, $id, '_key_deployment');
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
usleep(50000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param Document $document
|
||||
* @return Document
|
||||
*/
|
||||
protected function fixDocument(Document $document): Document
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'projects':
|
||||
/**
|
||||
* Bump version number.
|
||||
*/
|
||||
$document->setAttribute('version', '1.6.0');
|
||||
|
||||
// Add accessedAt attribute
|
||||
$document->setAttribute('accessedAt', DateTime::now());
|
||||
break;
|
||||
case 'functions':
|
||||
// Add scopes attribute
|
||||
$document->setAttribute('scopes', []);
|
||||
|
||||
// Add size attribute
|
||||
$document->setAttribute('size', 's-1vcpu-512m');
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
21
src/Appwrite/Utopia/Request/Filters/V18.php
Normal file
21
src/Appwrite/Utopia/Request/Filters/V18.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
|
||||
class V18 extends Filter
|
||||
{
|
||||
// Convert 1.5 params to 1.6
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'account.deleteMfaAuthenticator':
|
||||
unset($content['otp']);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
36
src/Appwrite/Utopia/Response/Filters/V18.php
Normal file
36
src/Appwrite/Utopia/Response/Filters/V18.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
|
||||
class V18 extends Filter
|
||||
{
|
||||
// Convert 1.6 Data format to 1.5 format
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
$parsedResponse = $content;
|
||||
|
||||
$parsedResponse = match($model) {
|
||||
Response::MODEL_FUNCTION => $this->parseFunction($content),
|
||||
Response::MODEL_PROJECT => $this->parseProject($content),
|
||||
default => $parsedResponse,
|
||||
};
|
||||
|
||||
return $parsedResponse;
|
||||
}
|
||||
|
||||
protected function parseFunction(array $content)
|
||||
{
|
||||
unset($content['scopes']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseProject(array $content)
|
||||
{
|
||||
unset($content['authMockNumbers']);
|
||||
unset($content['authSessionAlerts']);
|
||||
return $content;
|
||||
}
|
||||
}
|
|
@ -75,6 +75,12 @@ class Executor
|
|||
$runtimeId = "$projectId-$deploymentId-build";
|
||||
$route = "/runtimes";
|
||||
$timeout = (int) System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
|
||||
|
||||
// Remove after migration
|
||||
if ($version == 'v3') {
|
||||
$version = 'v4';
|
||||
}
|
||||
|
||||
$params = [
|
||||
'runtimeId' => $runtimeId,
|
||||
'source' => $source,
|
||||
|
@ -188,6 +194,13 @@ class Executor
|
|||
|
||||
$runtimeId = "$projectId-$deploymentId";
|
||||
$route = '/runtimes/' . $runtimeId . '/execution';
|
||||
|
||||
|
||||
// Remove after migration
|
||||
if ($version == 'v3') {
|
||||
$version = 'v4';
|
||||
}
|
||||
|
||||
$params = [
|
||||
'runtimeId' => $runtimeId,
|
||||
'variables' => $variables,
|
||||
|
|
|
@ -1589,6 +1589,27 @@ trait UsersBase
|
|||
], false);
|
||||
$this->assertEquals($user['headers']['status-code'], 201);
|
||||
|
||||
// Create JWT 0, with no session available
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['jwt']);
|
||||
$jwt0 = $response['body']['jwt'];
|
||||
|
||||
// Ensure JWT 0 works
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-jwt' => $jwt0,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($userId, $response['body']['$id']);
|
||||
|
||||
// Create two sessions
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
|
@ -1641,12 +1662,13 @@ trait UsersBase
|
|||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($userId, $response['body']['$id']);
|
||||
|
||||
// Create JWT 2 for latest session using default param
|
||||
// Create JWT 2 for latest session using 'current' param
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'duration' => 5
|
||||
'duration' => 5,
|
||||
'sessionId' => 'current'
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
@ -1696,6 +1718,27 @@ trait UsersBase
|
|||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
// Ensure JWT 0 works still even with no sessions
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId . '/sessions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'sessionId' => $session2Id
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-jwt' => $jwt0,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($userId, $response['body']['$id']);
|
||||
|
||||
// Cleanup after test
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, array_merge([
|
||||
|
|
51
tests/unit/Utopia/Request/Filters/V18Test.php
Normal file
51
tests/unit/Utopia/Request/Filters/V18Test.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Appwrite\Utopia\Request\Filters\V18;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class V18Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
protected $filter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V18();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function deleteMfaAuthenticatorProvider()
|
||||
{
|
||||
return [
|
||||
'remove otp' => [
|
||||
[
|
||||
'type' => 'totp',
|
||||
'otp' => 1230
|
||||
],
|
||||
[
|
||||
'type' => 'totp'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider deleteMfaAuthenticatorProvider
|
||||
*/
|
||||
public function testdeleteMfaAuthenticator(array $content, array $expected): void
|
||||
{
|
||||
$model = 'account.deleteMfaAuthenticator';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
84
tests/unit/Utopia/Response/Filters/V18Test.php
Normal file
84
tests/unit/Utopia/Response/Filters/V18Test.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V18;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class V18Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
protected $filter = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V18();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function functionProvider(): array
|
||||
{
|
||||
return [
|
||||
'remove scopes' => [
|
||||
[
|
||||
'scopes' => [
|
||||
'example_scope',
|
||||
'example_scope2',
|
||||
],
|
||||
],
|
||||
[
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider functionProvider
|
||||
*/
|
||||
public function testFunction(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_FUNCTION;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function projectProvider(): array
|
||||
{
|
||||
return [
|
||||
'remove authMockNumbers and authSessionAlerts' => [
|
||||
[
|
||||
'authMockNumbers' => [
|
||||
'example_mock_number',
|
||||
'example_mock_number2',
|
||||
],
|
||||
'authSessionAlerts' => [
|
||||
'example_alert',
|
||||
'example_alert2',
|
||||
],
|
||||
],
|
||||
[
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider projectProvider
|
||||
*/
|
||||
public function testProject(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_PROJECT;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue