diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d31cfed1d..4900fee3b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,6 @@ concurrency: env: COMPOSE_FILE: docker-compose.yml IMAGE: appwrite-dev - CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }} on: pull_request: @@ -248,11 +247,12 @@ jobs: TESTING=true VERSION=dev - - name: Cache Docker Image - uses: actions/cache@v5 + - name: Upload Docker Image + uses: actions/upload-artifact@v7 with: - key: ${{ env.CACHE_KEY }} + name: ${{ env.IMAGE }} path: /tmp/${{ env.IMAGE }}.tar + retention-days: 1 unit: name: Tests / Unit @@ -265,12 +265,11 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -306,12 +305,11 @@ jobs: - name: checkout uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -349,7 +347,7 @@ jobs: e2e_service: name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }} - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner || 'ubuntu-latest' }} needs: [build, matrix] permissions: contents: read @@ -384,16 +382,28 @@ jobs: Messaging, Migrations ] + include: + - service: Databases + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Sites + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Functions + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Avatars + runner: blacksmith-4vcpu-ubuntu-2404 + - service: Realtime + runner: blacksmith-4vcpu-ubuntu-2404 + - service: TablesDB + runner: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Set database environment run: | @@ -475,12 +485,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -527,12 +536,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 @@ -581,12 +589,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Load Cache - uses: actions/cache@v5 + - name: Download Docker Image + uses: actions/download-artifact@v7 with: - key: ${{ env.CACHE_KEY }} - path: /tmp/${{ env.IMAGE }}.tar - fail-on-cache-miss: true + name: ${{ env.IMAGE }} + path: /tmp - name: Login to Docker Hub uses: docker/login-action@v4 diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 24a1b28cdd..cef64ffdeb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -4,7 +4,6 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Validator\MockNumber; use Appwrite\Event\Delete; use Appwrite\Event\Mail; -use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; use Appwrite\Network\Platform; use Appwrite\Network\Validator\Email; @@ -30,7 +29,6 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; -use Utopia\Domains\Validator\PublicDomain; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; @@ -38,11 +36,9 @@ use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Hostname; use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; use Utopia\Validator\Nullable; use Utopia\Validator\Range; use Utopia\Validator\Text; -use Utopia\Validator\URL; use Utopia\Validator\WhiteList; Http::init() @@ -773,312 +769,6 @@ Http::delete('/v1/projects/:projectId') $response->noContent(); }); -// Webhooks - -Http::post('/v1/projects/:projectId/webhooks') - ->desc('Create webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'createWebhook', - description: '/docs/references/projects/create-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); - - $webhook = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), - 'name' => $name, - 'events' => $events, - 'url' => $url, - 'security' => $security, - 'httpUser' => $httpUser, - 'httpPass' => $httpPass, - 'signatureKey' => \bin2hex(\random_bytes(64)), - 'enabled' => $enabled, - ]); - - $webhook = $dbForPlatform->createDocument('webhooks', $webhook); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::get('/v1/projects/:projectId/webhooks') - ->desc('List webhooks') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'listWebhooks', - description: '/docs/references/projects/list-webhooks.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK_LIST, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhooks = $dbForPlatform->find('webhooks', [ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::limit(5000), - ]); - - $response->dynamic(new Document([ - 'webhooks' => $webhooks, - 'total' => $includeTotal ? count($webhooks) : 0, - ]), Response::MODEL_WEBHOOK_LIST); - }); - -Http::get('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Get webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'getWebhook', - description: '/docs/references/projects/get-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::put('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Update webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhook', - description: '/docs/references/projects/update-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook - ->setAttribute('name', $name) - ->setAttribute('events', $events) - ->setAttribute('url', $url) - ->setAttribute('security', $security) - ->setAttribute('httpUser', $httpUser) - ->setAttribute('httpPass', $httpPass) - ->setAttribute('enabled', $enabled); - - if ($enabled) { - $webhook->setAttribute('attempts', 0); - } - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') - ->desc('Update webhook signature key') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhookSignature', - description: '/docs/references/projects/update-webhook-signature.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64))); - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::delete('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Delete webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'deleteWebhook', - description: '/docs/references/projects/delete-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $dbForPlatform->deleteDocument('webhooks', $webhook->getId()); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->noContent(); - }); - // Keys Http::post('/v1/projects/:projectId/keys') diff --git a/app/controllers/general.php b/app/controllers/general.php index 1a099c4bde..341ac59f7a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -888,7 +888,7 @@ Http::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } - if (version_compare($requestFormat, '1.9.0', '<')) { + if (version_compare($requestFormat, '1.8.2', '<')) { $request->addFilter(new RequestV21()); } } diff --git a/app/init/constants.php b/app/init/constants.php index 7a484c7f4e..eab5ed91a4 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -47,7 +47,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4321; -const APP_VERSION_STABLE = '1.8.1'; +const APP_VERSION_STABLE = '1.8.2'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/app/init/models.php b/app/init/models.php index d432852660..6c90f08199 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -185,7 +185,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, ' Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT)); Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION)); Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false)); -Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false)); +Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true)); Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true)); Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false)); Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false)); diff --git a/app/init/resources.php b/app/init/resources.php index 4ebe705e73..2ed9e88e90 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -476,7 +476,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); -Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) { +Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -486,6 +486,20 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $projectId = $request->getHeader('x-appwrite-project', ''); } + // Backwards compatibility for new services, originally project resources + // These endpoints moved from /v1/projects/:projectId/ to /v1/ + // When accessed via the old alias path, extract projectId from the URI + $deprecatedProjectPathPrefix = '/v1/projects/'; + $route = $utopia->match($request); + if (!empty($route)) { + $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && + !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); + + if ($isDeprecatedAlias) { + $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + } + } + if (empty($projectId) || $projectId === 'console') { return $console; } @@ -493,7 +507,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); return $project; -}, ['dbForPlatform', 'request', 'console', 'authorization']); +}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']); Http::setResource('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { @@ -1072,16 +1086,21 @@ function getDevice(string $root, string $connection = ''): Device } } -Http::setResource('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - +Http::setResource('mode', function (Request $request, Document $project) { /** * Defines the mode for the request: * - 'default' => Requests for Client and Server Side * - 'admin' => Request from the Console on non-console projects */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); + $mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + if (!empty($projectId) && $project->getId() !== $projectId) { + $mode = APP_MODE_ADMIN; + } + + return $mode; +}, ['request', 'project']); Http::setResource('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ diff --git a/app/views/install/installer.phtml b/app/views/install/installer.phtml index 3cf2b77aa9..05bc1b80ed 100644 --- a/app/views/install/installer.phtml +++ b/app/views/install/installer.phtml @@ -9,6 +9,7 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp $defaultEmailCertificates = $defaultEmailCertificates ?? ''; $defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb'; $lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase; +$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql']; $isLocalInstall = $isLocalInstall ?? false; @@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time() data-default-secret-key="" data-default-assistant-openai-key="" data-default-database="" + data-enabled-databases="" data-dev-mode="true" diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css index 4601cfcb67..b1d8fe5089 100644 --- a/app/views/install/installer/css/styles.css +++ b/app/views/install/installer/css/styles.css @@ -11,9 +11,13 @@ --neutral-300: rgba(173, 173, 176, 1); --neutral-400: rgba(151, 151, 155, 1); --neutral-500: rgba(129, 129, 134, 1); + --neutral-600: rgba(108, 108, 113, 1); --neutral-700: rgba(86, 86, 92, 1); + --neutral-750: rgba(65, 65, 70, 1); --neutral-800: rgba(45, 45, 49, 1); + --neutral-850: rgba(29, 29, 33, 1); --neutral-900: rgba(25, 25, 28, 1); + --neutral-250: rgba(195, 195, 198, 1); /* Warning colors */ --web-orange-200: rgba(255, 213, 194, 1); @@ -172,6 +176,44 @@ --fgColor-neutral-weak: var(--fgcolor-neutral-weak); --fgColor-accent: var(--fgcolor-accent); --fgColor-on-accent: var(--fgcolor-on-accent); + + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --bgcolor-neutral-default: var(--neutral-900); + --bgcolor-neutral-primary: var(--neutral-850); + --bgcolor-neutral-secondary: var(--neutral-800); + --bgcolor-neutral-tertiary: var(--neutral-800); + --bgcolor-neutral-invert-weaker: var(--neutral-400); + --bgcolor-neutral-invert-weak: var(--neutral-300); + --bgcolor-success-weak: rgba(16, 185, 129, 0.12); + --bgcolor-warning-weaker: rgba(254, 124, 67, 0.08); + --bgcolor-warning-weak: rgba(254, 124, 67, 0.12); + --bgcolor-error-weaker: rgba(255, 69, 58, 0.08); + + --fgcolor-neutral-primary: var(--neutral-50); + --fgcolor-neutral-secondary: var(--neutral-250); + --fgcolor-neutral-tertiary: var(--neutral-500); + --fgcolor-neutral-weak: var(--neutral-600); + --fgcolor-on-accent: var(--neutral-0); + --fgcolor-on-invert: var(--neutral-900); + --fgcolor-on-success-weak: rgba(52, 211, 153, 1); + --fgcolor-warning: var(--web-orange-500); + --fgcolor-on-warning-weak: var(--web-orange-500); + --fgcolor-error: var(--web-red-500); + --fgcolor-on-error: var(--neutral-0); + + --border-neutral: var(--neutral-800); + --border-neutral-strong: var(--neutral-750); + --border-neutral-stronger: var(--neutral-600); + --border-focus: var(--neutral-600); + + --overlay-neutral-hover: rgba(255, 255, 255, 0.04); + --overlay-neutral-pressed: rgba(255, 255, 255, 0.08); + --overlay-scrim: rgba(0, 0, 0, 0.48); + } } .installer-toast-stack { @@ -1354,6 +1396,7 @@ body { align-items: center; justify-content: center; line-height: 0; + color: var(--fgcolor-neutral-weak); } .step-indicator svg { diff --git a/app/views/install/installer/icons/appwrite-logo.svg b/app/views/install/installer/icons/appwrite-logo.svg index f0f2fa5af0..af9378f8fc 100644 --- a/app/views/install/installer/icons/appwrite-logo.svg +++ b/app/views/install/installer/icons/appwrite-logo.svg @@ -1,13 +1,13 @@ diff --git a/app/views/install/installer/icons/indicator-inactive.svg b/app/views/install/installer/icons/indicator-inactive.svg index 78b5ef3ac0..550101da9b 100644 --- a/app/views/install/installer/icons/indicator-inactive.svg +++ b/app/views/install/installer/icons/indicator-inactive.svg @@ -1,3 +1,3 @@ diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js index bc08a8c8df..c531ecddce 100644 --- a/app/views/install/installer/js/modules/context.js +++ b/app/views/install/installer/js/modules/context.js @@ -2,6 +2,11 @@ const getBodyDataset = () => document.body?.dataset ?? {}; const isUpgradeMode = () => getBodyDataset().upgrade === 'true'; const getLockedDatabase = () => getBodyDataset().lockedDatabase || ''; + const getEnabledDatabases = () => { + const raw = getBodyDataset().enabledDatabases; + if (!raw) return ['mongodb', 'mariadb', 'postgresql']; + try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; } + }; const STEP_IDS = Object.freeze({ CONFIG_FILES: 'config-files', @@ -95,6 +100,7 @@ getBodyDataset, isUpgradeMode, getLockedDatabase, + getEnabledDatabases, STEP_IDS, STATUS, SSE_EVENTS, diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js index a6e784ada9..7f7b23e3fc 100644 --- a/app/views/install/installer/js/modules/progress.js +++ b/app/views/install/installer/js/modules/progress.js @@ -27,6 +27,7 @@ let activeInstall = null; let unloadGuard = null; + let sseSessionDetails = null; const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || ''; const withCsrfHeader = (headers = {}) => { @@ -524,6 +525,9 @@ inProgress: payload.message || payload.step, done: payload.message || payload.step }; + if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) { + sseSessionDetails = payload.details; + } progressState.set(step.id, { status: payload.status || STATUS.IN_PROGRESS, message: payload.message, @@ -599,7 +603,7 @@ }); if (!allDone) return; const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); - const sessionDetails = accountState?.details; + const sessionDetails = sseSessionDetails || accountState?.details; finalizeInstall(); notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => { setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0); @@ -740,7 +744,7 @@ } const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); - const sessionDetails = accountState?.details; + const sessionDetails = sseSessionDetails || accountState?.details; finalizeInstall(); notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => { setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0); diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js index d8b0621761..2a71d075cc 100644 --- a/app/views/install/installer/js/steps.js +++ b/app/views/install/installer/js/steps.js @@ -9,7 +9,8 @@ const { INSTALLATION_STEPS, clampStep, - isUpgradeMode + isUpgradeMode, + getEnabledDatabases } = Context; const { @@ -79,6 +80,19 @@ } }; + const applyEnabledDatabases = (root) => { + const enabled = getEnabledDatabases?.() || []; + const radios = root.querySelectorAll('input[name="database"]'); + radios.forEach((radio) => { + if (!enabled.includes(radio.value)) { + const card = radio.closest('.selector-card'); + if (card) { + card.remove(); + } + } + }); + }; + const bindDatabaseSelection = (root) => { const radios = root.querySelectorAll('input[name="database"]'); radios.forEach((radio) => { @@ -139,6 +153,8 @@ return; } + applyEnabledDatabases(root); + const lockedDatabase = getLockedDatabase?.() || ''; if (lockedDatabase) { lockDatabaseSelection(root, lockedDatabase); diff --git a/app/views/install/installer/templates/steps/step-1.phtml b/app/views/install/installer/templates/steps/step-1.phtml index ae7674c3bf..8f4a726158 100644 --- a/app/views/install/installer/templates/steps/step-1.phtml +++ b/app/views/install/installer/templates/steps/step-1.phtml @@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443'; $defaultEmailCertificates = $defaultEmailCertificates ?? ''; $defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? ''; $defaultDatabase = $defaultDatabase ?? 'mongodb'; +$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql']; $selectedDatabase = $lockedDatabase ?: $defaultDatabase; $isDatabaseLocked = !empty($lockedDatabase); +$mongoEnabled = in_array('mongodb', $enabledDatabases, true); +$mariaEnabled = in_array('mariadb', $enabledDatabases, true); +$postgresEnabled = in_array('postgresql', $enabledDatabases, true); $mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb'; $mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb'; $postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql'; @@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
+