Merge branch '1.6.x' into update-usage-test

This commit is contained in:
Christy Jacob 2025-04-15 17:43:10 +04:00 committed by GitHub
commit 3e0323dae8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 660 additions and 279 deletions

1
.env
View file

@ -15,6 +15,7 @@ _APP_SYSTEM_TEAM_EMAIL=team@appwrite.io
_APP_EMAIL_SECURITY=security@appwrite.io
_APP_EMAIL_CERTIFICATES=certificates@appwrite.io
_APP_SYSTEM_RESPONSE_FORMAT=
_APP_CUSTOM_DOMAIN_DENY_LIST=
_APP_OPTIONS_ABUSE=disabled
_APP_OPTIONS_ROUTER_PROTECTION=disabled
_APP_OPTIONS_FORCE_HTTPS=disabled

View file

@ -7,6 +7,7 @@ tasks:
docker pull composer
command: |
docker run --rm --interactive --tty \
--user "$(id -u):$(id -g)" \
--volume $PWD:/app \
composer install \
--ignore-platform-reqs \
@ -23,11 +24,3 @@ vscode:
extensions:
- ms-azuretools.vscode-docker
- zobo.php-intellisense
github:
# https://www.gitpod.io/docs/prebuilds#github-specific-configuration
prebuilds:
# enable for pull requests coming from forks (defaults to false)
pullRequestsFromForks: true
# add a check to pull requests (defaults to true)
addCheck: false

View file

@ -9,6 +9,7 @@ use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Executor\Executor;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\CLI;
@ -255,6 +256,8 @@ CLI::setResource('logError', function (Registry $register) {
};
}, ['register']);
CLI::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
$platform = new Appwrite();
$platform->init(Service::TYPE_TASK);

View file

@ -142,6 +142,16 @@ return [
'beta' => false,
'mock' => false,
],
'figma' => [
'name' => 'Figma',
'developers' => 'https://www.figma.com/developers/api#oauth2',
'icon' => 'icon-figma',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
'github' => [
'name' => 'GitHub',
'developers' => 'https://developer.github.com/',

View file

@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -1902,7 +1902,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"schema": {
"type": "string",
@ -1922,6 +1922,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -2778,7 +2779,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"schema": {
"type": "string",
@ -2798,6 +2799,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -1917,7 +1917,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"schema": {
"type": "string",
@ -1937,6 +1937,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -2786,7 +2787,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"schema": {
"type": "string",
@ -2806,6 +2807,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -19056,8 +19058,7 @@
"description": "Project Region.",
"x-example": "default",
"enum": [
"default",
"fra"
"default"
],
"x-enum-name": null,
"x-enum-keys": []
@ -20880,6 +20881,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -2464,7 +2464,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"schema": {
"type": "string",
@ -2484,6 +2484,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -2028,7 +2028,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"type": "string",
"x-example": "amazon",
@ -2047,6 +2047,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -2932,7 +2933,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"type": "string",
"x-example": "amazon",
@ -2951,6 +2952,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -2059,7 +2059,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"type": "string",
"x-example": "amazon",
@ -2078,6 +2078,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -2956,7 +2957,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"type": "string",
"x-example": "amazon",
@ -2975,6 +2976,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",
@ -19522,8 +19524,7 @@
"default": "default",
"x-example": "default",
"enum": [
"default",
"fra"
"default"
],
"x-enum-name": null,
"x-enum-keys": []
@ -21351,6 +21352,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.1",
"version": "1.6.2",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@ -2628,7 +2628,7 @@
"parameters": [
{
"name": "provider",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"description": "OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom.",
"required": true,
"type": "string",
"x-example": "amazon",
@ -2647,6 +2647,7 @@
"dropbox",
"etsy",
"facebook",
"figma",
"github",
"gitlab",
"google",

View file

@ -79,6 +79,15 @@ return [
'question' => 'Enter your Appwrite hostname',
'filter' => ''
],
[
'name' => '_APP_CUSTOM_DOMAIN_DENY_LIST',
'description' => 'List of reserved or prohibited domains when configuring custom domains.',
'introduction' => '',
'default' => 'example.com,test.com,app.example.com',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_DOMAIN_FUNCTIONS',
'description' => 'A domain to use for function preview URLs. Setting to empty turns off function preview URLs.',

View file

@ -869,7 +869,8 @@ App::put('/v1/functions/:functionId')
->inject('queueForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
->inject('executor')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github, Executor $executor) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@ -972,7 +973,6 @@ App::put('/v1/functions/:functionId')
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deployment'));
} catch (\Throwable $th) {
@ -1779,7 +1779,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) {
->inject('executor')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Executor $executor) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -1834,7 +1835,6 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
@ -1886,8 +1886,9 @@ App::post('/v1/functions/:functionId/executions')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('executor')
->inject('geodb')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb) {
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
@ -2160,7 +2161,6 @@ App::post('/v1/functions/:functionId/executions')
]);
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];

View file

@ -408,6 +408,7 @@ App::get('/v1/migrations/appwrite/report')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
$appwrite = new Appwrite($projectID, $endpoint, $key);
try {

View file

@ -138,6 +138,14 @@ App::post('/v1/projects')
$databases = Config::getParam('pools-database', []);
if ($region !== 'default') {
$databaseKeys = System::getEnv('_APP_DATABASE_KEYS', '');
$keys = explode(',', $databaseKeys);
$databases = array_filter($keys, function ($value) use ($region) {
return str_contains($value, $region);
});
}
$databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE');
$index = \array_search($databaseOverride, $databases);
if ($index !== false) {
@ -205,17 +213,17 @@ App::post('/v1/projects')
$dsn = new DSN('mysql://' . $dsn);
}
$adapter = $pools->get($dsn->getHost())->pop()->getResource();
$dbForProject = new Database($adapter, $cache);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$projectTables = !\in_array($dsn->getHost(), $sharedTables);
$sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1);
$sharedTablesV2 = !$projectTables && !$sharedTablesV1;
$sharedTables = $sharedTablesV1 || $sharedTablesV2;
if (!$sharedTablesV2) {
$adapter = $pools->get($dsn->getHost())->pop()->getResource();
$dbForProject = new Database($adapter, $cache);
if ($sharedTables) {
$dbForProject
->setSharedTables(true)

View file

@ -55,14 +55,25 @@ App::post('/v1/proxy/rules')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($domain === $mainDomain) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
}
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if ($functionsDomain != '' && str_ends_with($domain, $functionsDomain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions domain or it\'s subdomain to specific resource. Please use different domain.');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS');
$denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST');
if (!empty($denyListDomains)) {
$functionsDomain .= ',' . $denyListDomains;
}
$deniedDomains = array_map('trim', explode(',', $functionsDomain));
foreach ($deniedDomains as $deniedDomain) {
if (str_ends_with($domain, $deniedDomain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions domain or its subdomain to a specific resource. Please use a different domain.');
}
}
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {

View file

@ -1034,7 +1034,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->param('membershipId', '', new UID(), 'Membership ID.')
->param('roles', [], function (Document $project) {
if ($project->getId() === 'console') {
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
@ -1046,9 +1045,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('request')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -1069,6 +1069,21 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if ($project->getId() === 'console') {
// Quick check: fetch up to 2 owners to determine if only one exists
$ownersCount = $dbForProject->count(
collection: 'memberships',
queries: [Query::contains('roles', ['owner'])],
max: 2
);
// Prevent role change if there's only one owner left,
// the requester is that owner, and the new `$roles` no longer include 'owner'!
if ($ownersCount === 1 && $isOwner && !\in_array('owner', $roles)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'There must be at least one owner in the organization.');
}
}
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
}

View file

@ -50,7 +50,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname)
{
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
@ -72,11 +72,19 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
if ($route->isEmpty()) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$appDomainFunctions = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
if (!empty($appDomainFunctionsFallback) && \str_ends_with($host, $appDomainFunctionsFallback)) {
$appDomainFunctions = $appDomainFunctionsFallback;
}
if ($host === $appDomainFunctions) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
}
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))) {
if (\str_ends_with($host, $appDomainFunctions)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
}
@ -339,7 +347,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
]);
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
@ -503,9 +510,10 @@ App::init()
->inject('queueForEvents')
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('executor')
->inject('isResourceBlocked')
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
@ -513,7 +521,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@ -742,11 +750,12 @@ App::options()
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('project')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project) {
/*
* Appwrite Router
*/
@ -754,7 +763,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@ -871,17 +880,18 @@ App::error()
if (!empty($providerConfig) && $error->getCode() >= 400 && $error->getCode() < 500) {
// Register error logger
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
if (!empty($providerName) && $providerName === 'sentry') {
$key = $loggingProvider->getPassword();
$projectId = $loggingProvider->getUser() ?? '';
$host = 'https://' . $loggingProvider->getHost();
$sampleRate = $loggingProvider->getParam('sample', 0.01);
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.01);
$logger->setSample($sampleRate);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
@ -1061,10 +1071,11 @@ App::get('/robots.txt')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -1072,7 +1083,7 @@ App::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname);
}
});
@ -1089,10 +1100,11 @@ App::get('/humans.txt')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -1100,7 +1112,7 @@ App::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname);
}
});

View file

@ -390,7 +390,8 @@ App::init()
->inject('timelimit')
->inject('mode')
->inject('apiKey')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('plan')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey, array $plan) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@ -520,6 +521,10 @@ App::init()
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles());
$key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
@ -529,10 +534,10 @@ App::init()
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
$parts = explode('/', $cacheLog->getAttribute('resourceType'));
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
if ($type === 'bucket') {
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -573,8 +578,10 @@ App::init()
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'))
->send($data);
->setContentType($cacheLog->getAttribute('mimeType'));
if (!$isImageTransformation || !$isDisabled) {
$response->send($data);
}
} else {
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')

View file

@ -21,6 +21,7 @@ use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Request;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
@ -38,6 +39,7 @@ use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Storage\Device;
use Utopia\Storage\Device\AWS;
use Utopia\Storage\Device\Backblaze;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Device\Linode;
@ -46,6 +48,8 @@ use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\Wasabi;
use Utopia\Storage\Storage;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\Validator\Hostname;
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
@ -462,7 +466,9 @@ App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
};
}, ['pools', 'cache']);
App::setResource('cache', function (Group $pools) {
App::setResource('telemetry', fn () => new NoTelemetry());
App::setResource('cache', function (Group $pools, Telemetry $telemetry) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@ -470,12 +476,15 @@ App::setResource('cache', function (Group $pools) {
$adapters[] = $pools
->get($value)
->pop()
->getResource()
;
->getResource();
}
return new Cache(new Sharding($adapters));
}, ['pools']);
$cache = new Cache(new Sharding($adapters));
$cache->setTelemetry($telemetry);
return $cache;
}, ['pools', 'telemetry']);
App::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
@ -540,7 +549,12 @@ function getDevice(string $root, string $connection = ''): Device
switch ($device) {
case Storage::DEVICE_S3:
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl, $url);
if (!empty($url)) {
return new S3($root, $accessKey, $accessSecret, $url, $region, $acl);
} else {
return new AWS($root, $accessKey, $accessSecret, $bucket, $region, $acl);
}
// no break
case STORAGE::DEVICE_DO_SPACES:
$device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
@ -567,7 +581,12 @@ function getDevice(string $root, string $connection = ''): Device
$s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
$s3EndpointUrl = System::getEnv('_APP_STORAGE_S3_ENDPOINT', '');
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl, $s3EndpointUrl);
if (!empty($s3EndpointUrl)) {
return new S3($root, $s3AccessKey, $s3SecretKey, $s3EndpointUrl, $s3Region, $s3Acl);
} else {
return new AWS($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
}
// no break
case Storage::DEVICE_DO_SPACES:
$doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
@ -822,3 +841,5 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
return Key::decode($project, $key);
}, ['request', 'project']);
App::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));

View file

@ -168,7 +168,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:5.2.53
image: <?php echo $organization; ?>/console:5.2.58
restart: unless-stopped
networks:
- appwrite

View file

@ -18,6 +18,7 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Event\StatsUsageDump;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Cache\Adapter\Sharding;
@ -413,6 +414,8 @@ Server::setResource('logError', function (Registry $register, Document $project)
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');

View file

@ -51,11 +51,11 @@
"utopia-php/cache": "0.12.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.62.*",
"utopia-php/database": "0.65.*",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.3.*",
"utopia-php/fetch": "0.4.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
@ -63,7 +63,7 @@
"utopia-php/migration": "0.8.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.7.*",
"utopia-php/pools": "0.8.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.9.*",
"utopia-php/registry": "0.5.*",
@ -72,7 +72,7 @@
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.9.*",
"utopia-php/websocket": "0.1.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.1.*",
"dragonmantank/cron-expression": "3.3.2",
"phpmailer/phpmailer": "6.9.1",

208
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d2bec8137dcd84994121f89a29932d31",
"content-hash": "51ff891ef6cee8a3f8c4e5187b7fd479",
"packages": [
{
"name": "adhocore/jwt",
@ -709,16 +709,16 @@
},
{
"name": "google/protobuf",
"version": "v4.30.1",
"version": "v4.30.2",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24"
"reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/f29ba8a30dfd940efb3a8a75dc44446539101f24",
"reference": "f29ba8a30dfd940efb3a8a75dc44446539101f24",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/a4c4d8565b40b9f76debc9dfeb221412eacb8ced",
"reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced",
"shasum": ""
},
"require": {
@ -747,9 +747,9 @@
"proto"
],
"support": {
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.1"
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.2"
},
"time": "2025-03-13T21:08:17+00:00"
"time": "2025-03-26T18:01:50+00:00"
},
{
"name": "league/csv",
@ -1365,16 +1365,16 @@
},
{
"name": "open-telemetry/sdk",
"version": "1.2.2",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sdk.git",
"reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0"
"reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/37eec0fe47ddd627911f318f29b6cd48196be0c0",
"reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/0e7804c176c4b09d95b7985400aa38ce544cb7fc",
"reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc",
"shasum": ""
},
"require": {
@ -1451,7 +1451,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-01-29T21:40:28+00:00"
"time": "2025-04-08T09:55:41+00:00"
},
{
"name": "open-telemetry/sem-conv",
@ -2965,16 +2965,16 @@
},
{
"name": "tbachert/spi",
"version": "v1.0.2",
"version": "v1.0.3",
"source": {
"type": "git",
"url": "https://github.com/Nevay/spi.git",
"reference": "2ddfaf815dafb45791a61b08170de8d583c16062"
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Nevay/spi/zipball/2ddfaf815dafb45791a61b08170de8d583c16062",
"reference": "2ddfaf815dafb45791a61b08170de8d583c16062",
"url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"shasum": ""
},
"require": {
@ -3011,9 +3011,9 @@
],
"support": {
"issues": "https://github.com/Nevay/spi/issues",
"source": "https://github.com/Nevay/spi/tree/v1.0.2"
"source": "https://github.com/Nevay/spi/tree/v1.0.3"
},
"time": "2024-10-04T16:36:12+00:00"
"time": "2025-04-02T19:38:14+00:00"
},
{
"name": "thecodingmachine/safe",
@ -3497,16 +3497,16 @@
},
{
"name": "utopia-php/database",
"version": "0.62.1",
"version": "0.65.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "65dc51466c12552add10395900cdbb4728da4068"
"reference": "e589efdc5da1216523a758e8af358866d4fb563f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/65dc51466c12552add10395900cdbb4728da4068",
"reference": "65dc51466c12552add10395900cdbb4728da4068",
"url": "https://api.github.com/repos/utopia-php/database/zipball/e589efdc5da1216523a758e8af358866d4fb563f",
"reference": "e589efdc5da1216523a758e8af358866d4fb563f",
"shasum": ""
},
"require": {
@ -3514,7 +3514,8 @@
"ext-pdo": "*",
"php": ">=8.1",
"utopia-php/cache": "0.12.*",
"utopia-php/framework": "0.33.*"
"utopia-php/framework": "0.33.*",
"utopia-php/pools": "0.8.*"
},
"require-dev": {
"fakerphp/faker": "1.23.*",
@ -3546,9 +3547,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.62.1"
"source": "https://github.com/utopia-php/database/tree/0.65.0"
},
"time": "2025-03-24T08:27:18+00:00"
"time": "2025-04-14T07:39:01+00:00"
},
{
"name": "utopia-php/domains",
@ -3659,16 +3660,16 @@
},
{
"name": "utopia-php/fetch",
"version": "0.3.1",
"version": "0.4.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/fetch.git",
"reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0"
"reference": "65095dac14037db0c822fb5e209e5bd3187a0303"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0",
"reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/65095dac14037db0c822fb5e209e5bd3187a0303",
"reference": "65095dac14037db0c822fb5e209e5bd3187a0303",
"shasum": ""
},
"require": {
@ -3692,9 +3693,9 @@
"description": "A simple library that provides an interface for making HTTP Requests.",
"support": {
"issues": "https://github.com/utopia-php/fetch/issues",
"source": "https://github.com/utopia-php/fetch/tree/0.3.1"
"source": "https://github.com/utopia-php/fetch/tree/0.4.1"
},
"time": "2025-03-05T18:08:55+00:00"
"time": "2025-04-14T07:34:27+00:00"
},
{
"name": "utopia-php/framework",
@ -3745,16 +3746,16 @@
},
{
"name": "utopia-php/image",
"version": "0.8.0",
"version": "0.8.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca"
"reference": "6c736965177f9a9e71311e22b80cfa88511768e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/dcae5b1c6deb3ff6865f4e68f012b3709c289bca",
"reference": "dcae5b1c6deb3ff6865f4e68f012b3709c289bca",
"url": "https://api.github.com/repos/utopia-php/image/zipball/6c736965177f9a9e71311e22b80cfa88511768e9",
"reference": "6c736965177f9a9e71311e22b80cfa88511768e9",
"shasum": ""
},
"require": {
@ -3788,9 +3789,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.8.0"
"source": "https://github.com/utopia-php/image/tree/0.8.2"
},
"time": "2025-02-20T11:49:03+00:00"
"time": "2025-04-08T11:31:45+00:00"
},
{
"name": "utopia-php/locale",
@ -3950,16 +3951,16 @@
},
{
"name": "utopia-php/migration",
"version": "0.8.2",
"version": "0.8.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "aa3b7a508feb7090f487e7bf9cd71f5c92fbc7c1"
"reference": "84163e16edc0b2e64c34ad7b7c4cc5f05d762daf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/aa3b7a508feb7090f487e7bf9cd71f5c92fbc7c1",
"reference": "aa3b7a508feb7090f487e7bf9cd71f5c92fbc7c1",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/84163e16edc0b2e64c34ad7b7c4cc5f05d762daf",
"reference": "84163e16edc0b2e64c34ad7b7c4cc5f05d762daf",
"shasum": ""
},
"require": {
@ -3967,7 +3968,7 @@
"ext-curl": "*",
"ext-openssl": "*",
"php": ">=8.1",
"utopia-php/database": "0.62.*",
"utopia-php/database": "0.*.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/framework": "0.33.*",
"utopia-php/storage": "0.18.*"
@ -4000,9 +4001,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.8.2"
"source": "https://github.com/utopia-php/migration/tree/0.8.6"
},
"time": "2025-03-24T09:05:31+00:00"
"time": "2025-04-14T08:22:09+00:00"
},
{
"name": "utopia-php/orchestration",
@ -4106,16 +4107,16 @@
},
{
"name": "utopia-php/pools",
"version": "0.7.0",
"version": "0.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/pools.git",
"reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf"
"reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/ad64d45afda08ec8b29e2642a8d18075964d40bf",
"reference": "ad64d45afda08ec8b29e2642a8d18075964d40bf",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/60733929dc328e7ea47e800579c8bbf0d49df5ba",
"reference": "60733929dc328e7ea47e800579c8bbf0d49df5ba",
"shasum": ""
},
"require": {
@ -4152,9 +4153,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/pools/issues",
"source": "https://github.com/utopia-php/pools/tree/0.7.0"
"source": "https://github.com/utopia-php/pools/tree/0.8.0"
},
"time": "2025-03-18T03:55:33+00:00"
"time": "2025-03-19T10:22:03+00:00"
},
{
"name": "utopia-php/preloader",
@ -4211,23 +4212,23 @@
},
{
"name": "utopia-php/queue",
"version": "0.9.0",
"version": "0.9.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "077075f1d57afa430f76c35ed3bf4616e0eee8e7"
"reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/077075f1d57afa430f76c35ed3bf4616e0eee8e7",
"reference": "077075f1d57afa430f76c35ed3bf4616e0eee8e7",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32",
"reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/cli": "0.15.*",
"utopia-php/fetch": "^0.3.0",
"utopia-php/fetch": "0.4.*",
"utopia-php/framework": "0.33.*",
"utopia-php/telemetry": "0.1.*"
},
@ -4270,9 +4271,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.9.0"
"source": "https://github.com/utopia-php/queue/tree/0.9.1"
},
"time": "2025-03-13T12:22:41+00:00"
"time": "2025-03-28T19:49:36+00:00"
},
{
"name": "utopia-php/registry",
@ -4385,16 +4386,16 @@
},
{
"name": "utopia-php/swoole",
"version": "0.8.2",
"version": "0.8.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/swoole.git",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4"
"reference": "1af73dd3e73987cf729c7db399054e4a70befd99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4",
"reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/1af73dd3e73987cf729c7db399054e4a70befd99",
"reference": "1af73dd3e73987cf729c7db399054e4a70befd99",
"shasum": ""
},
"require": {
@ -4430,9 +4431,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/0.8.2"
"source": "https://github.com/utopia-php/swoole/tree/0.8.3"
},
"time": "2024-02-01T14:54:12+00:00"
"time": "2025-03-26T10:09:05+00:00"
},
{
"name": "utopia-php/system",
@ -4592,27 +4593,28 @@
},
{
"name": "utopia-php/websocket",
"version": "0.1.0",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/websocket.git",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5"
"reference": "629e53640b108eab43c7cc9ab375efade8622d43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5",
"reference": "51fcb86171400d8aa40d76c54593481fd273dab5",
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/629e53640b108eab43c7cc9ab375efade8622d43",
"reference": "629e53640b108eab43c7cc9ab375efade8622d43",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"laravel/pint": "^1.15",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.5.5",
"swoole/ide-helper": "4.6.6",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.2",
"vimeo/psalm": "^4.8.1",
"workerman/workerman": "^4.0"
"workerman/workerman": "4.1.*"
},
"type": "library",
"autoload": {
@ -4624,16 +4626,6 @@
"license": [
"MIT"
],
"authors": [
{
"name": "Eldad Fux",
"email": "eldad@appwrite.io"
},
{
"name": "Torsten Dittmann",
"email": "torsten@appwrite.io"
}
],
"description": "A simple abstraction for WebSocket servers.",
"keywords": [
"framework",
@ -4644,9 +4636,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/websocket/issues",
"source": "https://github.com/utopia-php/websocket/tree/0.1.0"
"source": "https://github.com/utopia-php/websocket/tree/0.3.0"
},
"time": "2021-12-20T10:50:09+00:00"
"time": "2025-03-28T01:11:13+00:00"
},
{
"name": "webmozart/assert",
@ -4775,16 +4767,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.40.9",
"version": "0.40.11",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "dbb45a5db22cdc3368fe2573c07ba6088f188fa4"
"reference": "0ec5f4a60c15e33e208bc3444ba6148b1d0f0027"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/dbb45a5db22cdc3368fe2573c07ba6088f188fa4",
"reference": "dbb45a5db22cdc3368fe2573c07ba6088f188fa4",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0ec5f4a60c15e33e208bc3444ba6148b1d0f0027",
"reference": "0ec5f4a60c15e33e208bc3444ba6148b1d0f0027",
"shasum": ""
},
"require": {
@ -4820,9 +4812,9 @@
"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/0.40.9"
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.11"
},
"time": "2025-03-17T18:39:14+00:00"
"time": "2025-03-26T10:53:16+00:00"
},
{
"name": "doctrine/annotations",
@ -5049,16 +5041,16 @@
},
{
"name": "laravel/pint",
"version": "v1.21.2",
"version": "v1.22.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "370772e7d9e9da087678a0edf2b11b6960e40558"
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558",
"reference": "370772e7d9e9da087678a0edf2b11b6960e40558",
"url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"shasum": ""
},
"require": {
@ -5069,9 +5061,9 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.72.0",
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.2",
"larastan/larastan": "^3.2.0",
"larastan/larastan": "^3.3.1",
"laravel-zero/framework": "^11.36.1",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3",
@ -5111,7 +5103,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-03-14T22:31:42+00:00"
"time": "2025-04-08T22:11:45+00:00"
},
{
"name": "matthiasmullie/minify",
@ -7154,16 +7146,16 @@
},
{
"name": "symfony/console",
"version": "v7.2.1",
"version": "v7.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3"
"reference": "e51498ea18570c062e7df29d05a7003585b19b88"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3",
"reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3",
"url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88",
"reference": "e51498ea18570c062e7df29d05a7003585b19b88",
"shasum": ""
},
"require": {
@ -7227,7 +7219,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.2.1"
"source": "https://github.com/symfony/console/tree/v7.2.5"
},
"funding": [
{
@ -7243,7 +7235,7 @@
"type": "tidelift"
}
],
"time": "2024-12-11T03:49:26+00:00"
"time": "2025-03-12T08:11:12+00:00"
},
{
"name": "symfony/filesystem",
@ -7758,16 +7750,16 @@
},
{
"name": "symfony/process",
"version": "v7.2.4",
"version": "v7.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf"
"reference": "87b7c93e57df9d8e39a093d32587702380ff045d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d",
"reference": "87b7c93e57df9d8e39a093d32587702380ff045d",
"shasum": ""
},
"require": {
@ -7799,7 +7791,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.2.4"
"source": "https://github.com/symfony/process/tree/v7.2.5"
},
"funding": [
{
@ -7815,7 +7807,7 @@
"type": "tidelift"
}
],
"time": "2025-02-05T08:33:46+00:00"
"time": "2025-03-13T12:21:46+00:00"
},
{
"name": "symfony/string",
@ -8134,7 +8126,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View file

@ -198,11 +198,14 @@ services:
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
- _APP_CUSTOM_DOMAIN_DENY_LIST
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:5.2.53
image: appwrite/console:5.2.58
restart: unless-stopped
networks:
- appwrite
@ -489,6 +492,8 @@ services:
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_DATABASE_SHARED_TABLES
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-worker-certificates:
entrypoint: worker-certificates
@ -1131,4 +1136,4 @@ volumes:
appwrite-certificates:
appwrite-functions:
appwrite-builds:
appwrite-config:
appwrite-config:

View file

@ -196,9 +196,9 @@ abstract class OAuth2
if (!empty($payload)) {
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$headers[] = 'Content-length: ' . \strlen($payload);
}
$headers[] = 'Content-length: ' . \strlen($payload);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Send the request & save response to $response

View file

@ -0,0 +1,178 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://www.figma.com/developers/api#oauth2
// https://www.figma.com/developers/api#authentication
class Figma extends OAuth2
{
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'current_user:read'
];
/**
* @return string
*/
public function getName(): string
{
return 'figma';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://www.figma.com/oauth?' . \http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state)
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$headers = [
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)
];
$this->tokens = \json_decode($this->request(
'POST',
'https://api.figma.com/v1/oauth/token',
$headers,
\http_build_query([
'redirect_uri' => $this->callback,
'code' => $code,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$headers = [
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)
];
$this->tokens = \json_decode($this->request(
'POST',
'https://api.figma.com/v1/oauth/refresh',
$headers,
\http_build_query([
'refresh_token' => $refreshToken
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['id'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified
*
* Figma requires email verification during signup,
* so if we have an email, it's verified
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
$email = $this->getUserEmail($accessToken);
return !empty($email);
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['handle'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$headers = ['Authorization: Bearer ' . $accessToken];
$user = $this->request(
'GET',
'https://api.figma.com/v1/me',
$headers
);
$this->user = \json_decode($user, true);
}
return $this->user;
}
}

View file

@ -351,6 +351,7 @@ class Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
@ -360,6 +361,7 @@ class Event
// Merge the base payload with any trimmed values
$payload = array_merge($this->preparePayload(), $this->trimPayload());
return $this->publisher->enqueue($queue, $payload);
}

View file

@ -10,7 +10,6 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
use Utopia\System\System;
class V19 extends Migration
{
@ -731,7 +730,7 @@ class V19 extends Migration
if (empty($document->getAttribute('scheduleId', null))) {
$schedule = $this->consoleDB->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'region' => $project->getAttribute('region'),
'resourceType' => 'function',
'resourceId' => $document->getId(),
'resourceInternalId' => $document->getInternalId(),

View file

@ -82,6 +82,14 @@ class V21 extends Migration
Console::warning("'type' from {$id}: {$th->getMessage()}");
}
break;
case 'migrations':
// Create destination attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'destination');
} catch (Throwable $th) {
Console::warning("'destination' from {$id}: {$th->getMessage()}");
}
break;
case 'schedules':
// Create data attribute
try {
@ -91,7 +99,14 @@ class V21 extends Migration
}
break;
case 'databases':
// Create originalId attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'originalId');
} catch (Throwable $th) {
Console::warning("'originalId' from {$id}: {$th->getMessage()}");
}
break;
case 'functions':
// Create scopes attribute
try {

View file

@ -47,15 +47,20 @@ class Maintenance extends Action
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
$dbForPlatform->foreach('projects', function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes
->setType(DELETE_TYPE_MAINTENANCE)
->setProject($project)
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->trigger();
}, [
Query::limit(100),
]);
$dbForPlatform->foreach(
'projects',
function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes
->setType(DELETE_TYPE_MAINTENANCE)
->setProject($project)
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
->trigger();
},
[
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::limit(100),
]
);
$queueForDeletes
->setType(DELETE_TYPE_MAINTENANCE)

View file

@ -103,8 +103,15 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
// Temporarly accepting both 'fra' and 'default'
// When all migrated, only use _APP_REGION with 'default' as default value
$regions = [System::getEnv('_APP_REGION', 'default')];
if (!in_array('default', $regions)) {
$regions[] = 'default';
}
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('region', $regions),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::equal('active', [true]),
]));
@ -153,8 +160,15 @@ abstract class ScheduleBase extends Action
$paginationQueries[] = Query::cursorAfter($latestDocument);
}
// Temporarly accepting both 'fra' and 'default'
// When all migrated, only use _APP_REGION with 'default' as default value
$regions = [System::getEnv('_APP_REGION', 'default')];
if (!in_array('default', $regions)) {
$regions[] = 'default';
}
$results = $dbForPlatform->find('schedules', \array_merge($paginationQueries, [
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('region', $regions),
Query::equal('resourceType', [static::getSupportedResource()]),
Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate),
]));

View file

@ -67,7 +67,8 @@ class StatsResources extends Action
* For each project that were accessed in last 24 hours
*/
$this->foreachDocument($this->dbForPlatform, 'projects', [
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours))
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours)),
Query::equal('region', [System::getEnv('_APP_REGION', 'default')])
], function ($project) use ($queue) {
$queue
->setProject($project)

View file

@ -74,7 +74,11 @@ class Audits extends Action
Console::info('Aggregating audit logs');
$event = $payload['event'] ?? '';
$auditPayload = $payload['payload'] ?? '';
$auditPayload = '';
if ($project->getId() === 'console') {
$auditPayload = $payload['payload'] ?? '';
}
$mode = $payload['mode'] ?? '';
$resource = $payload['resource'] ?? '';
$userAgent = $payload['userAgent'] ?? '';

View file

@ -59,8 +59,9 @@ class Builds extends Action
->inject('deviceForFunctions')
->inject('isResourceBlocked')
->inject('log')
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log));
->inject('executor')
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log, Executor $executor) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log, $executor));
}
/**
@ -76,10 +77,11 @@ class Builds extends Action
* @param Database $dbForProject
* @param Device $deviceForFunctions
* @param Log $log
* @param Executor $executor
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log): void
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log, Executor $executor): void
{
$payload = $message->getPayload() ?? [];
@ -100,7 +102,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log);
$this->buildDeployment($deviceForFunctions, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log, $executor);
break;
default:
@ -123,14 +125,13 @@ class Builds extends Action
* @param Document $deployment
* @param Document $template
* @param Log $log
* @param Executor $executor
* @return void
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, callable $isResourceBlocked, Log $log, Executor $executor): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$functionId = $function->getId();
$log->addTag('functionId', $function->getId());

View file

@ -563,21 +563,19 @@ class Databases extends Action
$start = \microtime(true);
try {
$documents = $database->deleteDocuments($collectionId, $queries);
$count = $database->deleteDocuments(
$collectionId,
$queries,
Database::DELETE_BATCH_SIZE,
$callback
);
} catch (\Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collectionId . ': ' . $th->getMessage());
$tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : '';
Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collectionId} {$tenant} :{$th->getMessage()}");
return;
}
if (\is_callable($callback)) {
foreach ($documents as $document) {
$callback($document);
}
}
$end = \microtime(true);
$count = \count($documents);
Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds");
}

View file

@ -31,6 +31,8 @@ use Utopia\System\System;
class Deletes extends Action
{
protected array $selects = ['$internalId', '$id', '$collection', '$permissions', '$updatedAt'];
public static function getName(): string
{
return 'deletes';
@ -53,12 +55,13 @@ class Deletes extends Action
->inject('deviceForBuilds')
->inject('deviceForCache')
->inject('certificates')
->inject('executor')
->inject('executionRetention')
->inject('auditRetention')
->inject('log')
->callback(
fn ($message, Document $project, Database $dbForPlatform, callable $getProjectDB, callable $getLogsDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log) =>
$this->action($message, $project, $dbForPlatform, $getProjectDB, $getLogsDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $executionRetention, $auditRetention, $log)
fn ($message, Document $project, Database $dbForPlatform, callable $getProjectDB, callable $getLogsDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Executor $executor, string $executionRetention, string $auditRetention, Log $log) =>
$this->action($message, $project, $dbForPlatform, $getProjectDB, $getLogsDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $executor, $executionRetention, $auditRetention, $log)
);
}
@ -66,7 +69,7 @@ class Deletes extends Action
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Document $project, Database $dbForPlatform, callable $getProjectDB, callable $getLogsDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, string $executionRetention, string $auditRetention, Log $log): void
public function action(Message $message, Document $project, Database $dbForPlatform, callable $getProjectDB, callable $getLogsDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Executor $executor, string $executionRetention, string $auditRetention, Log $log): void
{
$payload = $message->getPayload() ?? [];
@ -91,10 +94,10 @@ class Deletes extends Action
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project);
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project, $executor);
break;
case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
$this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project, $executor);
break;
case DELETE_TYPE_USERS:
$this->deleteUser($getProjectDB, $document, $project);
@ -180,10 +183,17 @@ class Deletes extends Action
*/
private function deleteSchedules(Database $dbForPlatform, callable $getProjectDB, string $datetime): void
{
// Temporarly accepting both 'fra' and 'default'
// When all migrated, only use _APP_REGION with 'default' as default value
$regions = [System::getEnv('_APP_REGION', 'default')];
if (!in_array('default', $regions)) {
$regions[] = 'default';
}
$this->listByGroup(
'schedules',
[
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
Query::equal('region', $regions),
Query::lessThanEqual('resourceUpdatedAt', $datetime),
Query::equal('active', [false]),
],
@ -359,7 +369,7 @@ class Deletes extends Action
$queries[] = Query::equal('resourceType', [$resourceType]);
}
$queries[] = Query::select(['$internalId', '$id', '$updatedAt']);
$queries[] = Query::select($this->selects);
$queries[] = Query::orderAsc();
$this->deleteByGroup(
@ -396,7 +406,7 @@ class Deletes extends Action
);
$queries = [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select([...$this->selects, 'accessedAt']),
Query::lessThan('accessedAt', $datetime),
Query::orderDesc('accessedAt'),
Query::orderDesc(),
@ -430,9 +440,11 @@ class Deletes extends Action
/** @var Database $dbForProject*/
$dbForProject = $getProjectDB($project);
$selects = [...$this->selects, 'time'];
// Delete Usage stats from projectDB
$this->deleteByGroup('stats', [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select($selects),
Query::equal('period', ['1h']),
Query::lessThan('time', $hourlyUsageRetentionDatetime),
Query::orderDesc('time'),
@ -445,7 +457,7 @@ class Deletes extends Action
// Delete Usage stats from logsDB
$this->deleteByGroup('stats', [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select($selects),
Query::equal('period', ['1h']),
Query::lessThan('time', $hourlyUsageRetentionDatetime),
Query::orderDesc('time'),
@ -482,21 +494,22 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
* @throws Structure
* @throws Exception
*/
private function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
* @throws Structure
* @throws Exception
*/
protected function deleteProjectsByTeam(Database $dbForPlatform, callable $getProjectDB, CertificatesAdapter $certificates, Document $document): void
{
$projects = $dbForPlatform->find('projects', [
Query::equal('teamInternalId', [$document->getInternalId()])
Query::equal('teamInternalId', [$document->getInternalId()]),
Query::equal('region', [System::getEnv('_APP_REGION', 'default')])
]);
foreach ($projects as $project) {
@ -742,7 +755,7 @@ class Deletes extends Action
// Delete Executions
$this->deleteByGroup('executions', [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select([...$this->selects, '$createdAt']),
Query::lessThan('$createdAt', $datetime),
Query::orderDesc('$createdAt'),
Query::orderDesc(),
@ -763,7 +776,7 @@ class Deletes extends Action
// Delete Sessions
$this->deleteByGroup('sessions', [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select([...$this->selects, '$createdAt']),
Query::lessThan('$createdAt', $expired),
Query::orderDesc('$createdAt'),
Query::orderDesc(),
@ -800,7 +813,7 @@ class Deletes extends Action
try {
$this->deleteByGroup(Audit::COLLECTION, [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select([...$this->selects, 'time']),
Query::lessThan('time', $auditRetention),
Query::orderDesc('time'),
Query::orderAsc(),
@ -816,10 +829,11 @@ class Deletes extends Action
* @param Device $deviceForBuilds
* @param Document $document function document
* @param Document $project
* @param Executor $executor
* @return void
* @throws Exception
*/
private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project): void
private function deleteFunction(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, CertificatesAdapter $certificates, Document $document, Document $project, Executor $executor): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@ -882,7 +896,7 @@ class Deletes extends Action
*/
Console::info("Deleting executions for function " . $functionId);
$this->deleteByGroup('executions', [
Query::select(['$internalId', '$id', '$updatedAt']),
Query::select($this->selects),
Query::equal('functionInternalId', [$functionInternalId]),
Query::orderAsc()
], $dbForProject);
@ -911,7 +925,7 @@ class Deletes extends Action
* Request executor to delete all deployment containers
*/
Console::info("Requesting executor to delete all deployment containers for function " . $functionId);
$this->deleteRuntimes($getProjectDB, $document, $project);
$this->deleteRuntimes($getProjectDB, $document, $project, $executor);
}
/**
@ -982,10 +996,11 @@ class Deletes extends Action
* @param Device $deviceForBuilds
* @param Document $document
* @param Document $project
* @param Executor $executor
* @return void
* @throws Exception
*/
private function deleteDeployment(callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void
private function deleteDeployment(callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project, Executor $executor): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@ -1013,7 +1028,7 @@ class Deletes extends Action
* Request executor to delete all deployment containers
*/
Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId);
$this->deleteRuntimes($getProjectDB, $document, $project);
$this->deleteRuntimes($getProjectDB, $document, $project, $executor);
}
/**
@ -1035,23 +1050,20 @@ class Deletes extends Action
/**
* deleteDocuments uses a cursor, we need to add a unique order by field or use default
*/
try {
$documents = $database->deleteDocuments($collection, $queries);
$count = $database->deleteDocuments(
$collection,
$queries,
Database::DELETE_BATCH_SIZE,
$callback
);
} catch (Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage());
$tenant = $database->getSharedTables() ? 'Tenant:'.$database->getTenant() : '';
Console::error("Failed to delete documents for collection:{$database->getNamespace()}_{$collection} {$tenant} :{$th->getMessage()}");
return;
}
if (\is_callable($callback)) {
foreach ($documents as $document) {
$callback($document);
}
}
$end = \microtime(true);
$count = \count($documents);
Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds");
}
@ -1168,13 +1180,12 @@ class Deletes extends Action
* @param callable $getProjectDB
* @param ?Document $function
* @param Document $project
* @param Executor $executor
* @return void
* @throws Exception
*/
private function deleteRuntimes(callable $getProjectDB, ?Document $function, Document $project): void
private function deleteRuntimes(callable $getProjectDB, ?Document $function, Document $project, Executor $executor): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$deleteByFunction = function (Document $function) use ($getProjectDB, $project, $executor) {
$this->listByGroup(
'deployments',

View file

@ -51,11 +51,12 @@ class Functions extends Action
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('log')
->inject('executor')
->inject('isResourceBlocked')
->callback(fn (Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked) => $this->action($project, $message, $dbForProject, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $log, $isResourceBlocked));
->callback(fn (Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, Executor $executor, callable $isResourceBlocked) => $this->action($project, $message, $dbForProject, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $log, $executor, $isResourceBlocked));
}
public function action(Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked): void
public function action(Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, Executor $executor, callable $isResourceBlocked): void
{
$payload = $message->getPayload() ?? [];
@ -146,6 +147,7 @@ class Functions extends Action
queueForEvents: $queueForEvents,
project: $project,
function: $function,
executor: $executor,
trigger: 'event',
path: '/',
method: 'POST',
@ -188,6 +190,7 @@ class Functions extends Action
queueForEvents: $queueForEvents,
project: $project,
function: $function,
executor: $executor,
trigger: 'http',
path: $path,
method: $method,
@ -212,6 +215,7 @@ class Functions extends Action
queueForEvents: $queueForEvents,
project: $project,
function: $function,
executor: $executor,
trigger: 'schedule',
path: $path,
method: $method,
@ -298,6 +302,7 @@ class Functions extends Action
* @param Event $queueForEvents
* @param Document $project
* @param Document $function
* @param Executor $executor
* @param string $trigger
* @param string $path
* @param string $method
@ -324,6 +329,7 @@ class Functions extends Action
Event $queueForEvents,
Document $project,
Document $function,
Executor $executor,
string $trigger,
string $path,
string $method,
@ -514,7 +520,6 @@ class Functions extends Action
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
$executionResponse = $executor->createExecution(
projectId: $project->getId(),

View file

@ -70,7 +70,6 @@ class StatsResources extends Action
}
if (empty($project->getAttribute('database'))) {
var_dump($payload);
return;
}

View file

@ -54,6 +54,8 @@ class Webhooks extends Action
$this->errors = [];
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}

View file

@ -21,17 +21,19 @@ class Executor
private bool $selfSigned = false;
private string $endpoint;
/**
* @var callable(string, string): string $endpoint
*/
private $endpointSelector;
protected array $headers;
public function __construct(string $endpoint)
/**
* @param callable(string, string): string $endpointSelector
*/
public function __construct(callable $endpointSelector)
{
if (!filter_var($endpoint, FILTER_VALIDATE_URL)) {
throw new Exception('Unsupported endpoint');
}
$this->endpoint = $endpoint;
$this->endpointSelector = $endpointSelector;
$this->headers = [
'content-type' => 'application/json',
'authorization' => 'Bearer ' . System::getEnv('_APP_EXECUTOR_SECRET', ''),
@ -92,7 +94,8 @@ class Executor
'timeout' => $timeout,
];
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@ -123,7 +126,8 @@ class Executor
'timeout' => $timeout
];
$this->call(self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$this->call($endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
}
/**
@ -139,7 +143,8 @@ class Executor
$runtimeId = "$projectId-$deploymentId";
$route = "/runtimes/$runtimeId";
$response = $this->call(self::METHOD_DELETE, $route, [
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_DELETE, $route, [
'x-opr-addressing-method' => 'broadcast'
], [], true, 30);
@ -227,7 +232,8 @@ class Executor
$requestTimeout = $timeout + 15;
}
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
$status = $response['headers']['status-code'];
if ($status >= 400) {
@ -235,7 +241,11 @@ class Executor
throw new \Exception($message, $status);
}
$response['body']['headers'] = \json_decode($response['body']['headers'] ?? '{}', true);
$headers = $response['body']['headers'] ?? [];
if (is_string($headers)) {
$headers = \json_decode($headers, true);
}
$response['body']['headers'] = $headers;
$response['body']['statusCode'] = \intval($response['body']['statusCode'] ?? 500);
$response['body']['duration'] = \floatval($response['body']['duration'] ?? 0);
$response['body']['startTime'] = \floatval($response['body']['startTime'] ?? \microtime(true));
@ -256,10 +266,10 @@ class Executor
* @return array|string
* @throws Exception
*/
public function call(string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15, callable $callback = null)
private function call(string $endpoint, string $method, string $path = '', array $headers = [], array $params = [], bool $decode = true, int $timeout = 15, callable $callback = null)
{
$headers = array_merge($this->headers, $headers);
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
$ch = curl_init($endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
$responseHeaders = [];
$responseStatus = -1;
$responseType = '';
@ -422,4 +432,9 @@ class Executor
return $output;
}
private function selectEndpoint(string $projectId, string $deploymentId): string
{
return call_user_func($this->endpointSelector, $projectId, $deploymentId);
}
}

View file

@ -37,6 +37,37 @@ trait TeamsBase
$teamUid = $response1['body']['$id'];
$teamName = $response1['body']['name'];
/**
* Test: Attempt to downgrade the only OWNER in an organization (should fail)
*/
if ($this->getProject()['$id'] === 'console') {
// Step 1: Fetch all team memberships — only one exists at this point
$response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
],
]);
// Step 2: Extract the membership ID of the only member (also the only OWNER)
$membershipID = $response['body']['memberships'][0]['$id'];
// Step 3: Attempt to downgrade the member's role to 'developer'
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipID, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'roles' => ['developer']
]);
// Step 4: Assert failure — cannot remove the only OWNER from a team
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('general_argument_invalid', $response['body']['type']);
$this->assertEquals('There must be at least one owner in the organization.', $response['body']['message']);
}
$teamId = ID::unique();
$response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',