mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge pull request #11650 from appwrite/feat-public-project-keys
Feat: Public keys API
This commit is contained in:
@@ -3,13 +3,6 @@
|
||||
// List of scopes for organization (teams) API keys
|
||||
|
||||
return [
|
||||
"platforms.read" => [
|
||||
"description" => 'Access to read project\'s platforms',
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
'Access to create, update, and delete project\'s platforms',
|
||||
],
|
||||
"projects.read" => [
|
||||
"description" => 'Access to read organization\'s projects',
|
||||
],
|
||||
@@ -17,13 +10,6 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete projects in organization",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" => 'Access to read project\'s API keys',
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s API keys",
|
||||
],
|
||||
"devKeys.read" => [
|
||||
"description" => 'Access to read project\'s development keys',
|
||||
],
|
||||
|
||||
@@ -188,4 +188,20 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -5,28 +5,18 @@ use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
@@ -769,288 +759,6 @@ Http::delete('/v1/projects/:projectId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/keys')
|
||||
->desc('Create key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'createKey',
|
||||
description: '/docs/references/projects/create-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
// TODO: When migrating to Platform API, mark keyId required for consistency
|
||||
->param('keyId', 'unique()', fn (Database $dbForPlatform) => new CustomId($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true, ['dbForPlatform'])->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = new Document([
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes,
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $dbForPlatform->createDocument('keys', $key);
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys')
|
||||
->desc('List keys')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'listKeys',
|
||||
description: '/docs/references/projects/list-keys.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$keys = $dbForPlatform->find('keys', $queries);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Get key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'getKey',
|
||||
description: '/docs/references/projects/get-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Update key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'updateKey',
|
||||
description: '/docs/references/projects/update-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('scopes', $scopes)
|
||||
->setAttribute('expire', $expire);
|
||||
|
||||
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Delete key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'keys',
|
||||
name: 'deleteKey',
|
||||
description: '/docs/references/projects/delete-key.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $keyId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$key = $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($key->isEmpty()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('keys', $key->getId());
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// JWT Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/jwts')
|
||||
|
||||
Generated
+68
-84
@@ -2708,16 +2708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.7",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"reference": "01933e626c3de76bea1e22641e205e78f6a34342",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2785,7 +2785,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-05T11:16:58+00:00"
|
||||
"time": "2026-03-30T12:55:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
@@ -4325,16 +4325,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.16",
|
||||
"version": "0.34.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f"
|
||||
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
|
||||
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
|
||||
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4373,9 +4373,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.16"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.18"
|
||||
},
|
||||
"time": "2026-03-20T10:39:07+00:00"
|
||||
"time": "2026-04-07T08:06:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4696,16 +4696,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8"
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/068ee46228f0c3972e6b569f2c86b6c80fe583d8",
|
||||
"reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4741,9 +4741,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.0"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
|
||||
},
|
||||
"time": "2026-03-31T14:44:23+00:00"
|
||||
"time": "2026-04-08T04:11:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -5502,16 +5502,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.14.0",
|
||||
"version": "1.17.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
"reference": "8888a9fd11260d389874424268ecbe0d956eb550"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8888a9fd11260d389874424268ecbe0d956eb550",
|
||||
"reference": "8888a9fd11260d389874424268ecbe0d956eb550",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5547,22 +5547,22 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.6"
|
||||
},
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
"time": "2026-04-08T05:37:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
"version": "v7.19.2",
|
||||
"version": "v7.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paratestphp/paratest.git",
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
|
||||
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
|
||||
"reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5586,7 +5586,7 @@
|
||||
"ext-pcntl": "*",
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"phpstan/phpstan": "^2.1.40",
|
||||
"phpstan/phpstan": "^2.1.44",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.4",
|
||||
"phpstan/phpstan-phpunit": "^2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.10",
|
||||
@@ -5630,7 +5630,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.20.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5642,7 +5642,7 @@
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-09T14:33:17+00:00"
|
||||
"time": "2026-03-29T15:46:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
@@ -6258,11 +6258,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.44",
|
||||
"version": "2.1.46",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6307,7 +6307,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -6657,16 +6657,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.14",
|
||||
"version": "12.5.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0"
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0",
|
||||
"reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6688,7 +6688,7 @@
|
||||
"sebastian/cli-parser": "^4.2.0",
|
||||
"sebastian/comparator": "^7.1.4",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
"sebastian/environment": "^8.0.4",
|
||||
"sebastian/exporter": "^7.0.2",
|
||||
"sebastian/global-state": "^8.0.2",
|
||||
"sebastian/object-enumerator": "^7.0.0",
|
||||
@@ -6735,31 +6735,15 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://phpunit.de/sponsors.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
|
||||
"type": "tidelift"
|
||||
"url": "https://phpunit.de/sponsoring.html",
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-18T12:38:40+00:00"
|
||||
"time": "2026-04-08T03:04:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -6832,16 +6816,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "7.1.4",
|
||||
"version": "7.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6"
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6",
|
||||
"reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6900,7 +6884,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6920,7 +6904,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-24T09:28:48+00:00"
|
||||
"time": "2026-04-08T04:43:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
@@ -7744,16 +7728,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.7",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7810,7 +7794,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.7"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7830,7 +7814,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-06T14:06:22+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
@@ -8164,16 +8148,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.5",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8205,7 +8189,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.5"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8225,20 +8209,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v8.0.6",
|
||||
"version": "v8.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8295,7 +8279,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.6"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8315,7 +8299,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-09T10:14:57+00:00"
|
||||
"time": "2026-03-30T15:14:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "textalk/websocket",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Create extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/project/keys')
|
||||
->httpAlias('/v1/projects/:projectId/keys')
|
||||
->desc('Create project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].create')
|
||||
->label('audits.event', 'project.key.create')
|
||||
->label('audits.resource', 'project.key/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'createKey',
|
||||
description: <<<EOT
|
||||
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
],
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$keyId = ($keyId == 'unique()') ? ID::unique() : $keyId;
|
||||
|
||||
$key = new Document([
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [],
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->createDocument('keys', $key));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Delete project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].delete')
|
||||
->label('audits.event', 'project.key.delete')
|
||||
->label('audits.resource', 'project.key/{request.keyId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'deleteKey',
|
||||
description: <<<EOT
|
||||
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $keyId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Event $queueForEvents,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('keys', $key->getId()))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
|
||||
};
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Get project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'getKey',
|
||||
description: <<<EOT
|
||||
Get a key by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $keyId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectKey';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/project/keys/:keyId')
|
||||
->httpAlias('/v1/projects/:projectId/keys/:keyId')
|
||||
->desc('Update project key')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.write')
|
||||
->label('event', 'keys.[keyId].update')
|
||||
->label('audits.event', 'project.key.update')
|
||||
->label('audits.resource', 'project.key/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'updateKey',
|
||||
description: <<<EOT
|
||||
Update a key by its unique ID. Use this endpoint to update the name, scopes, or expiration time of an API key.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new Datetime()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
public function action(
|
||||
string $keyId,
|
||||
string $name,
|
||||
?array $scopes,
|
||||
?string $expire,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->getDocument('keys', $keyId));
|
||||
|
||||
if ($key->isEmpty() || $key->getAttribute('resourceType', '') !== 'projects' || $key->getAttribute('resourceInternalId', '') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::KEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$updates = new Document([
|
||||
'name' => $name,
|
||||
'scopes' => $scopes ?? [],
|
||||
'expire' => $expire,
|
||||
]);
|
||||
|
||||
try {
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('keys', $key->getId(), $updates));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
|
||||
$queueForEvents->setParam('keyId', $key->getId());
|
||||
|
||||
$response->dynamic($key, Response::MODEL_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Keys;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listProjectKeys';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/keys')
|
||||
->httpAlias('/v1/projects/:projectId/keys')
|
||||
->desc('List project keys')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'keys.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'keys',
|
||||
name: 'listKeys',
|
||||
description: <<<EOT
|
||||
Get a list of all API keys from the current project.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_KEY_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $queries
|
||||
*/
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $includeTotal,
|
||||
Document $project,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('keys', [
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
]));
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$keys = $authorization->skip(fn () => $dbForPlatform->find('keys', $queries));
|
||||
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT)) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class Create extends Action
|
||||
->setHttpPath('/v1/project/platforms/android')
|
||||
->desc('Create project Android platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Update extends Action
|
||||
->setHttpPath('/v1/project/platforms/android/:platformId')
|
||||
->desc('Update project Android platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -35,7 +35,7 @@ class Create extends Action
|
||||
->setHttpPath('/v1/project/platforms/apple')
|
||||
->desc('Create project Apple platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Update extends Action
|
||||
->setHttpPath('/v1/project/platforms/apple/:platformId')
|
||||
->desc('Update project Apple platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Delete extends Action
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Delete project platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].delete')
|
||||
->label('audits.event', 'project.platform.delete')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -32,7 +32,7 @@ class Get extends Action
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Get project platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.read')
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
|
||||
@@ -35,7 +35,7 @@ class Create extends Action
|
||||
->setHttpPath('/v1/project/platforms/linux')
|
||||
->desc('Create project Linux platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Update extends Action
|
||||
->setHttpPath('/v1/project/platforms/linux/:platformId')
|
||||
->desc('Update project Linux platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -43,7 +43,7 @@ class Create extends Action
|
||||
->httpAlias('/v1/projects/:projectId/platforms')
|
||||
->desc('Create project web platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -35,7 +35,7 @@ class Update extends Action
|
||||
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
|
||||
->desc('Update project web platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -35,7 +35,7 @@ class Create extends Action
|
||||
->setHttpPath('/v1/project/platforms/windows')
|
||||
->desc('Create project Windows platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].create')
|
||||
->label('audits.event', 'project.platform.create')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -33,7 +33,7 @@ class Update extends Action
|
||||
->setHttpPath('/v1/project/platforms/windows/:platformId')
|
||||
->desc('Update project Windows platform')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].update')
|
||||
->label('audits.event', 'project.platform.update')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
|
||||
@@ -36,7 +36,7 @@ class XList extends Action
|
||||
->httpAlias('/v1/projects/:projectId/platforms')
|
||||
->desc('List project platforms')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.read')
|
||||
->label('scope', 'platforms.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
|
||||
@@ -34,7 +34,7 @@ class Delete extends Action
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'variables.[variableId].delete')
|
||||
->label('audits.event', 'project.variable.delete')
|
||||
->label('audits.resource', 'project.variable/{response.$id}')
|
||||
->label('audits.resource', 'project.variable/{request.variableId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'variables',
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Project\Http\Init;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Get as GetKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Update as UpdateKey;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Keys\XList as ListKeys;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform;
|
||||
@@ -43,6 +48,13 @@ class Http extends Service
|
||||
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
|
||||
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
|
||||
|
||||
// Keys
|
||||
$this->addAction(CreateKey::getName(), new CreateKey());
|
||||
$this->addAction(ListKeys::getName(), new ListKeys());
|
||||
$this->addAction(GetKey::getName(), new GetKey());
|
||||
$this->addAction(DeleteKey::getName(), new DeleteKey());
|
||||
$this->addAction(UpdateKey::getName(), new UpdateKey());
|
||||
|
||||
// Platforms
|
||||
$this->addAction(DeletePlatform::getName(), new DeletePlatform());
|
||||
$this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
|
||||
|
||||
@@ -379,7 +379,11 @@ class Migrations extends Action
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write'
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ class V21 extends Filter
|
||||
case 'webhooks.create':
|
||||
$content = $this->fillWebhookid($content);
|
||||
break;
|
||||
case 'project.createKey':
|
||||
$content = $this->fillKeyId($content);
|
||||
break;
|
||||
case 'project.createVariable':
|
||||
$content = $this->fillVariableId($content);
|
||||
break;
|
||||
@@ -122,6 +125,12 @@ class V21 extends Filter
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillKeyId(array $content): array
|
||||
{
|
||||
$content['keyId'] = $content['keyId'] ?? 'unique()';
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function fillVariableId(array $content): array
|
||||
{
|
||||
$content['variableId'] = $content['variableId'] ?? 'unique()';
|
||||
|
||||
@@ -164,7 +164,11 @@ trait ProjectCustom
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write'
|
||||
'project.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,806 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Appwrite\Tests\Async;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
|
||||
trait KeysBase
|
||||
{
|
||||
use Async;
|
||||
|
||||
// =========================================================================
|
||||
// Create key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testCreateKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'My API Key',
|
||||
['users.read', 'users.write'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertNotEmpty($key['body']['$id']);
|
||||
$this->assertSame('My API Key', $key['body']['name']);
|
||||
$this->assertSame(['users.read', 'users.write'], $key['body']['scopes']);
|
||||
$this->assertNotEmpty($key['body']['secret']);
|
||||
$this->assertSame('', $key['body']['expire']);
|
||||
$this->assertSame('', $key['body']['accessedAt']);
|
||||
$this->assertSame([], $key['body']['sdks']);
|
||||
|
||||
$dateValidator = new DatetimeValidator();
|
||||
$this->assertSame(true, $dateValidator->isValid($key['body']['$createdAt']));
|
||||
$this->assertSame(true, $dateValidator->isValid($key['body']['$updatedAt']));
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($key['body']['$id']);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($key['body']['$id'], $get['body']['$id']);
|
||||
$this->assertSame('My API Key', $get['body']['name']);
|
||||
$this->assertSame(['users.read', 'users.write'], $get['body']['scopes']);
|
||||
|
||||
// Verify via LIST
|
||||
$list = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertGreaterThanOrEqual(1, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithExpire(): void
|
||||
{
|
||||
$expire = '2030-01-01T00:00:00.000+00:00';
|
||||
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Expiring Key',
|
||||
['users.read'],
|
||||
$expire,
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame($expire, $key['body']['expire']);
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($key['body']['$id']);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($expire, $get['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithNullScopes(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Null Scopes Key',
|
||||
null,
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame([], $key['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testCreateKeyWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Auth Key',
|
||||
['users.read'],
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyInvalidId(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
'!invalid-id!',
|
||||
'Invalid ID Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $key['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyMissingName(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
null,
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyInvalidScope(): void
|
||||
{
|
||||
$response = $this->createKey(
|
||||
ID::unique(),
|
||||
'Invalid Scope Key',
|
||||
['invalid.scope'],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateKeyDuplicateId(): void
|
||||
{
|
||||
$keyId = ID::unique();
|
||||
|
||||
$key = $this->createKey(
|
||||
$keyId,
|
||||
'Key Dup 1',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
|
||||
// Attempt to create with same ID
|
||||
$duplicate = $this->createKey(
|
||||
$keyId,
|
||||
'Key Dup 2',
|
||||
['users.write'],
|
||||
);
|
||||
|
||||
$this->assertSame(409, $duplicate['headers']['status-code']);
|
||||
$this->assertSame('key_already_exists', $duplicate['body']['type']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testCreateKeyCustomId(): void
|
||||
{
|
||||
$customId = 'my-custom-key-id';
|
||||
|
||||
$key = $this->createKey(
|
||||
$customId,
|
||||
'Custom ID Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame($customId, $key['body']['$id']);
|
||||
|
||||
// Verify via GET
|
||||
$get = $this->getKey($customId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($customId, $get['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($customId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Original Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Update name, scopes, and expire
|
||||
$expire = '2031-06-15T12:00:00.000+00:00';
|
||||
$updated = $this->updateKey($keyId, 'Updated Key', ['users.write', 'databases.read'], $expire);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame($keyId, $updated['body']['$id']);
|
||||
$this->assertSame('Updated Key', $updated['body']['name']);
|
||||
$this->assertSame(['users.write', 'databases.read'], $updated['body']['scopes']);
|
||||
$this->assertSame($expire, $updated['body']['expire']);
|
||||
|
||||
// Verify update persisted via GET
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame('Updated Key', $get['body']['name']);
|
||||
$this->assertSame(['users.write', 'databases.read'], $get['body']['scopes']);
|
||||
$this->assertSame($expire, $get['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyName(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Name Before',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Name After', ['users.read']);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame('Name After', $updated['body']['name']);
|
||||
$this->assertSame(['users.read'], $updated['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyScopes(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Scopes Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Scopes Key', ['databases.read', 'databases.write']);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame(['databases.read', 'databases.write'], $updated['body']['scopes']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeySetExpire(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Expire Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$this->assertSame('', $key['body']['expire']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$expire = '2032-12-31T23:59:59.000+00:00';
|
||||
$updated = $this->updateKey($keyId, 'No Expire Key', ['users.read'], $expire);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame($expire, $updated['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyRemoveExpire(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Expire Key',
|
||||
['users.read'],
|
||||
'2030-01-01T00:00:00.000+00:00',
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Remove expire by setting to null
|
||||
$updated = $this->updateKey($keyId, 'Expire Key', ['users.read'], null);
|
||||
|
||||
$this->assertSame(200, $updated['headers']['status-code']);
|
||||
$this->assertSame('', $updated['body']['expire']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Auth Update Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt update without authentication
|
||||
$response = $this->updateKey($keyId, 'Updated Name', ['users.read'], null, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testUpdateKeyNotFound(): void
|
||||
{
|
||||
$updated = $this->updateKey('non-existent-id', 'New Name', ['users.read']);
|
||||
|
||||
$this->assertSame(404, $updated['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $updated['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateKeyInvalidScope(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Invalid Scope Update',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$updated = $this->updateKey($keyId, 'Invalid Scope Update', ['invalid.scope']);
|
||||
|
||||
$this->assertSame(400, $updated['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Get key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testGetKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Get Test Key',
|
||||
['users.read', 'databases.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
$get = $this->getKey($keyId);
|
||||
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame($keyId, $get['body']['$id']);
|
||||
$this->assertSame('Get Test Key', $get['body']['name']);
|
||||
$this->assertSame(['users.read', 'databases.read'], $get['body']['scopes']);
|
||||
$this->assertNotEmpty($get['body']['secret']);
|
||||
$this->assertSame('', $get['body']['expire']);
|
||||
$this->assertSame('', $get['body']['accessedAt']);
|
||||
$this->assertSame([], $get['body']['sdks']);
|
||||
|
||||
$dateValidator = new DatetimeValidator();
|
||||
$this->assertSame(true, $dateValidator->isValid($get['body']['$createdAt']));
|
||||
$this->assertSame(true, $dateValidator->isValid($get['body']['$updatedAt']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testGetKeyNotFound(): void
|
||||
{
|
||||
$get = $this->getKey('non-existent-id');
|
||||
|
||||
$this->assertSame(404, $get['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $get['body']['type']);
|
||||
}
|
||||
|
||||
public function testGetKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Auth Get Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt GET without authentication
|
||||
$response = $this->getKey($keyId, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// List keys tests
|
||||
// =========================================================================
|
||||
|
||||
public function testListKeys(): void
|
||||
{
|
||||
// Create multiple keys
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Alpha',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Beta',
|
||||
['databases.read'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
$key3 = $this->createKey(
|
||||
ID::unique(),
|
||||
'List Key Gamma',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key3['headers']['status-code']);
|
||||
|
||||
// List all
|
||||
$list = $this->listKeys(null, true);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertGreaterThanOrEqual(3, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(3, \count($list['body']['keys']));
|
||||
$this->assertIsArray($list['body']['keys']);
|
||||
|
||||
// Verify structure of returned keys
|
||||
foreach ($list['body']['keys'] as $key) {
|
||||
$this->assertArrayHasKey('$id', $key);
|
||||
$this->assertArrayHasKey('$createdAt', $key);
|
||||
$this->assertArrayHasKey('$updatedAt', $key);
|
||||
$this->assertArrayHasKey('name', $key);
|
||||
$this->assertArrayHasKey('scopes', $key);
|
||||
$this->assertArrayHasKey('secret', $key);
|
||||
$this->assertArrayHasKey('expire', $key);
|
||||
$this->assertArrayHasKey('accessedAt', $key);
|
||||
$this->assertArrayHasKey('sdks', $key);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
$this->deleteKey($key3['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithLimit(): void
|
||||
{
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Limit Key 1',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Limit Key 2',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
// List with limit 1
|
||||
$list = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertCount(1, $list['body']['keys']);
|
||||
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithoutTotal(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'No Total Key',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
|
||||
// List with total=false
|
||||
$list = $this->listKeys(null, false);
|
||||
|
||||
$this->assertSame(200, $list['headers']['status-code']);
|
||||
$this->assertSame(0, $list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(1, \count($list['body']['keys']));
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysCursorPagination(): void
|
||||
{
|
||||
$key1 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Cursor Key 1',
|
||||
['users.read'],
|
||||
);
|
||||
$this->assertSame(201, $key1['headers']['status-code']);
|
||||
|
||||
$key2 = $this->createKey(
|
||||
ID::unique(),
|
||||
'Cursor Key 2',
|
||||
['users.write'],
|
||||
);
|
||||
$this->assertSame(201, $key2['headers']['status-code']);
|
||||
|
||||
// Get first page with limit 1
|
||||
$page1 = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $page1['headers']['status-code']);
|
||||
$this->assertCount(1, $page1['body']['keys']);
|
||||
$cursorId = $page1['body']['keys'][0]['$id'];
|
||||
|
||||
// Get next page using cursor
|
||||
$page2 = $this->listKeys([
|
||||
Query::limit(1)->toString(),
|
||||
Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(200, $page2['headers']['status-code']);
|
||||
$this->assertCount(1, $page2['body']['keys']);
|
||||
$this->assertNotEquals($cursorId, $page2['body']['keys'][0]['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($key1['body']['$id']);
|
||||
$this->deleteKey($key2['body']['$id']);
|
||||
}
|
||||
|
||||
public function testListKeysWithoutAuthentication(): void
|
||||
{
|
||||
$response = $this->listKeys(null, null, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testListKeysInvalidCursor(): void
|
||||
{
|
||||
$list = $this->listKeys([
|
||||
Query::cursorAfter(new Document(['$id' => 'non-existent-id']))->toString(),
|
||||
], true);
|
||||
|
||||
$this->assertSame(400, $list['headers']['status-code']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Delete key tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDeleteKey(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Verify it exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
|
||||
// Delete
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
$this->assertEmpty($delete['body']);
|
||||
|
||||
// Verify it no longer exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(404, $get['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $get['body']['type']);
|
||||
}
|
||||
|
||||
public function testDeleteKeyNotFound(): void
|
||||
{
|
||||
$delete = $this->deleteKey('non-existent-id');
|
||||
|
||||
$this->assertSame(404, $delete['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $delete['body']['type']);
|
||||
}
|
||||
|
||||
public function testDeleteKeyWithoutAuthentication(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete Auth Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Attempt DELETE without authentication
|
||||
$response = $this->deleteKey($keyId, false);
|
||||
|
||||
$this->assertSame(401, $response['headers']['status-code']);
|
||||
|
||||
// Verify it still exists
|
||||
$get = $this->getKey($keyId);
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteKey($keyId);
|
||||
}
|
||||
|
||||
public function testDeleteKeyRemovedFromList(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Delete List Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// Get list count before delete
|
||||
$listBefore = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $listBefore['headers']['status-code']);
|
||||
$countBefore = $listBefore['body']['total'];
|
||||
|
||||
// Delete
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
|
||||
// Get list count after delete
|
||||
$listAfter = $this->listKeys(null, true);
|
||||
$this->assertSame(200, $listAfter['headers']['status-code']);
|
||||
$this->assertSame($countBefore - 1, $listAfter['body']['total']);
|
||||
|
||||
// Verify the deleted key is not in the list
|
||||
$ids = \array_column($listAfter['body']['keys'], '$id');
|
||||
$this->assertNotContains($keyId, $ids);
|
||||
}
|
||||
|
||||
public function testDeleteKeyDoubleDelete(): void
|
||||
{
|
||||
$key = $this->createKey(
|
||||
ID::unique(),
|
||||
'Double Delete Key',
|
||||
['users.read'],
|
||||
);
|
||||
|
||||
$this->assertSame(201, $key['headers']['status-code']);
|
||||
$keyId = $key['body']['$id'];
|
||||
|
||||
// First delete succeeds
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
|
||||
// Second delete returns 404
|
||||
$delete = $this->deleteKey($keyId);
|
||||
$this->assertSame(404, $delete['headers']['status-code']);
|
||||
$this->assertSame('key_not_found', $delete['body']['type']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
protected function createKey(string $keyId, ?string $name, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [
|
||||
'keyId' => $keyId,
|
||||
'scopes' => $scopes,
|
||||
];
|
||||
|
||||
if ($name !== null) {
|
||||
$params['name'] = $name;
|
||||
}
|
||||
|
||||
if ($expire !== null) {
|
||||
$params['expire'] = $expire;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_POST, '/project/keys', $headers, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $scopes
|
||||
*/
|
||||
protected function updateKey(string $keyId, ?string $name = null, ?array $scopes = null, ?string $expire = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [];
|
||||
|
||||
if ($name !== null) {
|
||||
$params['name'] = $name;
|
||||
}
|
||||
|
||||
if ($scopes !== null) {
|
||||
$params['scopes'] = $scopes;
|
||||
}
|
||||
|
||||
if ($expire !== null) {
|
||||
$params['expire'] = $expire;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_PUT, '/project/keys/' . $keyId, $headers, $params);
|
||||
}
|
||||
|
||||
protected function getKey(string $keyId, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/keys/' . $keyId, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $queries
|
||||
*/
|
||||
protected function listKeys(?array $queries, ?bool $total, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/keys', $headers, [
|
||||
'queries' => $queries,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function deleteKey(string $keyId, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_DELETE, '/project/keys/' . $keyId, $headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class KeysConsoleClientTest extends Scope
|
||||
{
|
||||
use KeysBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class KeysCustomServerTest extends Scope
|
||||
{
|
||||
use KeysBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
}
|
||||
@@ -120,6 +120,7 @@ trait ProjectsBase
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.8.0',
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
|
||||
@@ -3161,6 +3161,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.8.0',
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
@@ -3246,6 +3247,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.8.0',
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Test 2',
|
||||
'scopes' => ['users.read'],
|
||||
@@ -3621,6 +3623,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.8.0',
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key For Deletion',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
|
||||
Reference in New Issue
Block a user