mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
commit
124c71b66b
3431 changed files with 72666 additions and 12162 deletions
34
.env
34
.env
|
|
@ -11,8 +11,9 @@ _APP_SYSTEM_RESPONSE_FORMAT=
|
|||
_APP_OPTIONS_ABUSE=disabled
|
||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
_APP_DOMAIN=demo.appwrite.io
|
||||
_APP_DOMAIN_TARGET=demo.appwrite.io
|
||||
_APP_DOMAIN=localhost
|
||||
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
||||
_APP_DOMAIN_TARGET=localhost
|
||||
_APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
|
|
@ -63,14 +64,14 @@ _APP_STORAGE_PREVIEW_LIMIT=20000000
|
|||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
_APP_FUNCTIONS_TIMEOUT=900
|
||||
_APP_FUNCTIONS_BUILD_TIMEOUT=900
|
||||
_APP_FUNCTIONS_CONTAINERS=10
|
||||
_APP_FUNCTIONS_CPUS=0
|
||||
_APP_FUNCTIONS_MEMORY=0
|
||||
_APP_FUNCTIONS_MEMORY_SWAP=0
|
||||
_APP_FUNCTIONS_INACTIVE_THRESHOLD=60
|
||||
OPEN_RUNTIMES_NETWORK=appwrite_runtimes
|
||||
_APP_FUNCTIONS_CPUS=1
|
||||
_APP_FUNCTIONS_MEMORY=1024
|
||||
_APP_FUNCTIONS_INACTIVE_THRESHOLD=600
|
||||
_APP_FUNCTIONS_MAINTENANCE_INTERVAL=600
|
||||
_APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes
|
||||
_APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://appwrite-executor/v1
|
||||
_APP_EXECUTOR_HOST=http://proxy/v1
|
||||
_APP_FUNCTIONS_RUNTIMES=
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
|
|
@ -78,12 +79,21 @@ _APP_MAINTENANCE_RETENTION_ABUSE=86400
|
|||
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=5
|
||||
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
|
||||
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
|
||||
_APP_USAGE_STATS=enabled
|
||||
_APP_LOGGING_PROVIDER=
|
||||
_APP_LOGGING_CONFIG=
|
||||
_APP_GRAPHQL_MAX_BATCH_SIZE=10
|
||||
_APP_GRAPHQL_MAX_COMPLEXITY=250
|
||||
_APP_GRAPHQL_MAX_DEPTH=3
|
||||
DOCKERHUB_PULL_USERNAME=
|
||||
DOCKERHUB_PULL_PASSWORD=
|
||||
DOCKERHUB_PULL_EMAIL=
|
||||
_APP_DOCKER_HUB_USERNAME=
|
||||
_APP_DOCKER_HUB_PASSWORD=
|
||||
_APP_VCS_GITHUB_APP_NAME=
|
||||
_APP_VCS_GITHUB_PRIVATE_KEY=""
|
||||
_APP_VCS_GITHUB_APP_ID=
|
||||
_APP_VCS_GITHUB_CLIENT_ID=
|
||||
_APP_VCS_GITHUB_CLIENT_SECRET=
|
||||
_APP_VCS_GITHUB_WEBHOOK_SECRET=
|
||||
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
|
||||
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
|
||||
_APP_ASSISTANT_OPENAI_API_KEY=
|
||||
46
.github/workflows/publish.yml
vendored
Normal file
46
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: "Build and Publish"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- appwrite-*
|
||||
|
||||
jobs:
|
||||
build-publish:
|
||||
name: Build and Publish
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
submodules: recursive
|
||||
ref: feat-db-pools-master
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: appwrite/cloud
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build & Publish to DockerHub
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
|
||||
VITE_GA_PROJECT=G-L7G2B6PLDS
|
||||
VITE_CONSOLE_MODE=cloud
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -49,7 +49,9 @@ jobs:
|
|||
sleep 30
|
||||
|
||||
- name: Doctor
|
||||
run: docker compose exec -T appwrite doctor
|
||||
run: |
|
||||
docker compose logs appwrite
|
||||
docker compose exec -T appwrite doctor
|
||||
|
||||
- name: Environment Variables
|
||||
run: docker compose exec -T appwrite vars
|
||||
|
|
|
|||
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -1,4 +1,4 @@
|
|||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = 2.3.4
|
||||
branch = 3.0.1
|
||||
|
|
|
|||
29
CHANGES.md
29
CHANGES.md
|
|
@ -1,5 +1,33 @@
|
|||
# Version 1.4.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add error attribute to indexes and attributes [#4575](https://github.com/appwrite/appwrite/pull/4575)
|
||||
- Add new index validation rules [#5710](https://github.com/appwrite/appwrite/pull/5710)
|
||||
- Added support for disallowing passwords that contain personal data [#5371](https://github.com/appwrite/appwrite/pull/5371)
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fix cascading deletes across multiple levels [DB #269](https://github.com/utopia-php/database/pull/269)
|
||||
- Fix identical two-way keys not throwing duplicate exceptions [DB #273](https://github.com/utopia-php/database/pull/273)
|
||||
- Fix search wildcards [DB #279](https://github.com/utopia-php/database/pull/279)
|
||||
- Fix permissions returning as an object instead of list [DB #281](https://github.com/utopia-php/database/pull/281)
|
||||
- Fix missing collection not found error [DB #282](https://github.com/utopia-php/database/pull/282)
|
||||
|
||||
## Changes
|
||||
|
||||
- Improve permission indexes [DB #248](https://github.com/utopia-php/database/pull/248)
|
||||
- Validators back-ported to Utopia [#5439](https://github.com/appwrite/appwrite/pull/5439)
|
||||
|
||||
# Version 1.3.8
|
||||
|
||||
## Changes
|
||||
|
||||
- Replace Appwrite executor with OpenRuntimes Executor [#4650](https://github.com/appwrite/appwrite/pull/4650)
|
||||
- Add `_APP_CONNECTIONS_MAX` env var [#4673](https://github.com/appwrite/appwrite/pull/4673)
|
||||
- Increase Traefik TCP + file limits [#4673](https://github.com/appwrite/appwrite/pull/4673)
|
||||
- Store build output file size [#4844](https://github.com/appwrite/appwrite/pull/4844)
|
||||
|
||||
## Bugs
|
||||
- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809)
|
||||
|
||||
|
|
@ -93,7 +121,6 @@
|
|||
- Get default region from environment on project create [#4780](https://github.com/appwrite/appwrite/pull/4780)
|
||||
- Fix french translation [#4782](https://github.com/appwrite/appwrite/pull/4782)
|
||||
- Fix max mimetype size [#4814](https://github.com/appwrite/appwrite/pull/4814)
|
||||
|
||||
## Bugs
|
||||
- Fix invited account verified status [#4776](https://github.com/appwrite/appwrite/pull/4776)
|
||||
|
||||
|
|
|
|||
|
|
@ -240,8 +240,8 @@ Appwrite stack is a combination of a variety of open-source technologies and too
|
|||
|
||||
- Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data).
|
||||
- MariaDB - for database storage and queries.
|
||||
- InfluxDB - for managing stats and time-series based data.
|
||||
- Statsd - for sending data over UDP protocol (using Telegraf).
|
||||
- InfluxDB - for managing stats and time-series based data
|
||||
- Statsd - for sending data over UDP protocol (using Telegraf)
|
||||
- ClamAV - for validating and scanning storage files.
|
||||
- Imagemagick - for manipulating and managing image media files.
|
||||
- Webp - for better compression of images on supporting clients.
|
||||
|
|
|
|||
27
Dockerfile
27
Dockerfile
|
|
@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
|
|||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM appwrite/base:0.2.2 as final
|
||||
FROM appwrite/base:0.4.3 as final
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ ENV _APP_SERVER=swoole \
|
|||
_APP_LOCALE=en \
|
||||
_APP_WORKER_PER_CORE= \
|
||||
_APP_DOMAIN=localhost \
|
||||
_APP_DOMAIN_FUNCTIONS=functions.localhost \
|
||||
_APP_DOMAIN_TARGET=localhost \
|
||||
_APP_HOME=https://appwrite.io \
|
||||
_APP_EDITION=community \
|
||||
|
|
@ -122,7 +123,13 @@ ENV _APP_SERVER=swoole \
|
|||
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 \
|
||||
_APP_MAINTENANCE_INTERVAL=86400 \
|
||||
_APP_LOGGING_PROVIDER= \
|
||||
_APP_LOGGING_CONFIG=
|
||||
_APP_LOGGING_CONFIG= \
|
||||
_APP_VCS_GITHUB_APP_NAME= \
|
||||
_APP_VCS_GITHUB_PRIVATE_KEY= \
|
||||
_APP_VCS_GITHUB_APP_ID= \
|
||||
_APP_VCS_GITHUB_CLIENT_ID= \
|
||||
_APP_VCS_GITHUB_CLIENT_SECRET= \
|
||||
_APP_VCS_GITHUB_WEBHOOK_SECRET=
|
||||
|
||||
RUN \
|
||||
if [ "$DEBUG" == "true" ]; then \
|
||||
|
|
@ -136,6 +143,7 @@ COPY --from=node /usr/local/src/console/build /usr/src/code/console
|
|||
|
||||
# Add Source Code
|
||||
COPY ./app /usr/src/code/app
|
||||
COPY ./public /usr/src/code/public
|
||||
COPY ./bin /usr/local/bin
|
||||
COPY ./docs /usr/src/code/docs
|
||||
COPY ./src /usr/src/code/src
|
||||
|
|
@ -156,12 +164,11 @@ RUN mkdir -p /storage/uploads && \
|
|||
|
||||
# Executables
|
||||
RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/maintenance && \
|
||||
chmod +x /usr/local/bin/maintenance && \
|
||||
chmod +x /usr/local/bin/usage && \
|
||||
chmod +x /usr/local/bin/install && \
|
||||
chmod +x /usr/local/bin/migrate && \
|
||||
chmod +x /usr/local/bin/realtime && \
|
||||
chmod +x /usr/local/bin/executor && \
|
||||
chmod +x /usr/local/bin/schedule && \
|
||||
chmod +x /usr/local/bin/sdks && \
|
||||
chmod +x /usr/local/bin/specs && \
|
||||
|
|
@ -176,7 +183,17 @@ RUN chmod +x /usr/local/bin/doctor && \
|
|||
chmod +x /usr/local/bin/worker-builds && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-webhooks
|
||||
chmod +x /usr/local/bin/worker-webhooks && \
|
||||
chmod +x /usr/local/bin/worker-migrations
|
||||
|
||||
# Cloud Executabless
|
||||
RUN chmod +x /usr/local/bin/hamster && \
|
||||
chmod +x /usr/local/bin/volume-sync && \
|
||||
chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \
|
||||
chmod +x /usr/local/bin/patch-delete-project-collections && \
|
||||
chmod +x /usr/local/bin/clear-card-cache && \
|
||||
chmod +x /usr/local/bin/calc-users-stats && \
|
||||
chmod +x /usr/local/bin/calc-tier-stats
|
||||
|
||||
# Letsencrypt Permissions
|
||||
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ docker run -it --rm \
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
|
@ -78,7 +78,7 @@ docker run -it --rm ^
|
|||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
|
@ -88,7 +88,7 @@ docker run -it --rm `
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ docker run -it --rm \
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
|
@ -87,7 +87,7 @@ docker run -it --rm ^
|
|||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
|
@ -97,7 +97,7 @@ docker run -it --rm `
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.3.8
|
||||
appwrite/appwrite:1.4.0
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
| 1.1.x | :white_check_mark: |
|
||||
| 1.2.x | :white_check_mark: |
|
||||
| 1.3.x | :white_check_mark: |
|
||||
| 1.4.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
|||
195
app/cli.php
195
app/cli.php
|
|
@ -3,30 +3,193 @@
|
|||
require_once __DIR__ . '/init.php';
|
||||
require_once __DIR__ . '/controllers/general.php';
|
||||
|
||||
use Utopia\App;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
|
||||
Authorization::disable();
|
||||
|
||||
$cli = new CLI();
|
||||
CLI::setResource('register', fn()=>$register);
|
||||
|
||||
include 'tasks/doctor.php';
|
||||
include 'tasks/maintenance.php';
|
||||
include 'tasks/install.php';
|
||||
include 'tasks/migrate.php';
|
||||
include 'tasks/sdks.php';
|
||||
include 'tasks/specs.php';
|
||||
include 'tasks/ssl.php';
|
||||
include 'tasks/vars.php';
|
||||
include 'tasks/usage.php';
|
||||
CLI::setResource('cache', function ($pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = $pools
|
||||
->get($value)
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['pools']);
|
||||
|
||||
CLI::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
CLI::setResource('dbForConsole', function ($pools, $cache) {
|
||||
$sleep = 3;
|
||||
$maxAttempts = 5;
|
||||
$attempts = 0;
|
||||
$ready = false;
|
||||
|
||||
do {
|
||||
$attempts++;
|
||||
try {
|
||||
// Prepare database connection
|
||||
$dbAdapter = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource();
|
||||
|
||||
$dbForConsole = new Database($dbAdapter, $cache);
|
||||
$dbForConsole->setNamespace('_console');
|
||||
|
||||
// Ensure tables exist
|
||||
$collections = Config::getParam('collections', [])['console'];
|
||||
$last = \array_key_last($collections);
|
||||
|
||||
if (!($dbForConsole->exists($dbForConsole->getDefaultDatabase(), $last))) { /** TODO cache ready variable using registry */
|
||||
throw new Exception('Tables not ready yet.');
|
||||
}
|
||||
|
||||
$ready = true;
|
||||
} catch (\Exception $err) {
|
||||
Console::warning($err->getMessage());
|
||||
$pools->get('console')->reclaim();
|
||||
sleep($sleep);
|
||||
}
|
||||
} while ($attempts < $maxAttempts && !$ready);
|
||||
|
||||
if (!$ready) {
|
||||
throw new Exception("Console is not ready yet. Please try again later.");
|
||||
}
|
||||
|
||||
return $dbForConsole;
|
||||
}, ['pools', 'cache']);
|
||||
|
||||
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
$getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$databaseName = $project->getAttribute('database');
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
return $database;
|
||||
}
|
||||
|
||||
$dbAdapter = $pools
|
||||
->get($databaseName)
|
||||
->pop()
|
||||
->getResource();
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
$databases[$databaseName] = $database;
|
||||
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
return $getProjectDB;
|
||||
}, ['pools', 'dbForConsole', 'cache']);
|
||||
|
||||
CLI::setResource('influxdb', function (Registry $register) {
|
||||
$client = $register->get('influxdb'); /** @var InfluxDB\Client $client */
|
||||
$attempts = 0;
|
||||
$max = 10;
|
||||
$sleep = 1;
|
||||
|
||||
do { // check if telegraf database is ready
|
||||
try {
|
||||
$attempts++;
|
||||
$database = $client->selectDB('telegraf');
|
||||
if (in_array('telegraf', $client->listDatabases())) {
|
||||
break; // leave the do-while if successful
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
|
||||
if ($attempts >= $max) {
|
||||
throw new \Exception('InfluxDB database not ready yet');
|
||||
}
|
||||
sleep($sleep);
|
||||
}
|
||||
} while ($attempts < $max);
|
||||
return $database;
|
||||
}, ['register']);
|
||||
|
||||
CLI::setResource('queueForFunctions', function (Group $pools) {
|
||||
return new Func($pools->get('queue')->pop()->getResource());
|
||||
}, ['pools']);
|
||||
|
||||
CLI::setResource('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Usage stats log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
};
|
||||
}, ['register']);
|
||||
|
||||
$platform = new Appwrite();
|
||||
$platform->init(Service::TYPE_CLI);
|
||||
|
||||
$cli = $platform->getCli();
|
||||
|
||||
$cli
|
||||
->task('version')
|
||||
->desc('Get the server version')
|
||||
->action(function () {
|
||||
Console::log(App::getEnv('_APP_VERSION', 'UNKNOWN'));
|
||||
->error()
|
||||
->inject('error')
|
||||
->action(function (Throwable $error) {
|
||||
Console::error($error->getMessage());
|
||||
});
|
||||
|
||||
$cli->run();
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
|
||||
return [
|
||||
// Codes based on: https://github.com/matomo-org/device-detector/blob/master/Parser/Client/Browser.php
|
||||
'aa' => __DIR__ . '/browsers/avant.png',
|
||||
'an' => __DIR__ . '/browsers/android-webview-beta.png',
|
||||
'ch' => __DIR__ . '/browsers/chrome.png',
|
||||
'ci' => __DIR__ . '/browsers/chrome.png', //Chrome Mobile iOS
|
||||
'cm' => __DIR__ . '/browsers/chrome.png', //Chrome Mobile
|
||||
'cr' => __DIR__ . '/browsers/chromium.png',
|
||||
'ff' => __DIR__ . '/browsers/firefox.png',
|
||||
'sf' => __DIR__ . '/browsers/safari.png',
|
||||
'mf' => __DIR__ . '/browsers/safari.png',
|
||||
'ps' => __DIR__ . '/browsers/edge.png',
|
||||
'oi' => __DIR__ . '/browsers/edge.png',
|
||||
'om' => __DIR__ . '/browsers/opera-mini.png',
|
||||
'op' => __DIR__ . '/browsers/opera.png',
|
||||
'on' => __DIR__ . '/browsers/opera.png',
|
||||
'aa' => ['name' => 'Avant Browser', 'path' => __DIR__ . '/browsers/avant.png'],
|
||||
'an' => ['name' => 'Android WebView Beta', 'path' => __DIR__ . '/browsers/android-webview-beta.png'],
|
||||
'ch' => ['name' => 'Google Chrome', 'path' => __DIR__ . '/browsers/chrome.png'],
|
||||
'ci' => ['name' => 'Google Chrome (iOS)', 'path' => __DIR__ . '/browsers/chrome.png'],
|
||||
'cm' => ['name' => 'Google Chrome (Mobile)', 'path' => __DIR__ . '/browsers/chrome.png'],
|
||||
'cr' => ['name' => 'Chromium', 'path' => __DIR__ . '/browsers/chromium.png'],
|
||||
'ff' => ['name' => 'Mozilla Firefox', 'path' => __DIR__ . '/browsers/firefox.png'],
|
||||
'sf' => ['name' => 'Safari', 'path' => __DIR__ . '/browsers/safari.png'],
|
||||
'mf' => ['name' => 'Mobile Safari', 'path' => __DIR__ . '/browsers/safari.png'],
|
||||
'ps' => ['name' => 'Microsoft Edge', 'path' => __DIR__ . '/browsers/edge.png'],
|
||||
'oi' => ['name' => 'Microsoft Edge (iOS)', 'path' => __DIR__ . '/browsers/edge.png'],
|
||||
'om' => ['name' => 'Opera Mini', 'path' => __DIR__ . '/browsers/opera-mini.png'],
|
||||
'op' => ['name' => 'Opera', 'path' => __DIR__ . '/browsers/opera.png'],
|
||||
'on' => ['name' => 'Opera (Next)', 'path' => __DIR__ . '/browsers/opera.png'],
|
||||
|
||||
|
||||
/*
|
||||
'36' => '360 Phone Browser',
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'amex' => __DIR__ . '/credit-cards/amex.png',
|
||||
'argencard' => __DIR__ . '/credit-cards/argencard.png',
|
||||
'cabal' => __DIR__ . '/credit-cards/cabal.png',
|
||||
'censosud' => __DIR__ . '/credit-cards/consosud.png',
|
||||
'diners' => __DIR__ . '/credit-cards/diners.png',
|
||||
'discover' => __DIR__ . '/credit-cards/discover.png',
|
||||
'elo' => __DIR__ . '/credit-cards/elo.png',
|
||||
'hipercard' => __DIR__ . '/credit-cards/hipercard.png',
|
||||
'jcb' => __DIR__ . '/credit-cards/jcb.png',
|
||||
'mastercard' => __DIR__ . '/credit-cards/mastercard.png',
|
||||
'naranja' => __DIR__ . '/credit-cards/naranja.png',
|
||||
'targeta-shopping' => __DIR__ . '/credit-cards/tarjeta-shopping.png',
|
||||
'union-china-pay' => __DIR__ . '/credit-cards/union-china-pay.png',
|
||||
'visa' => __DIR__ . '/credit-cards/visa.png',
|
||||
'mir' => __DIR__ . '/credit-cards/mir.png',
|
||||
'maestro' => __DIR__ . '/credit-cards/maestro.png',
|
||||
];
|
||||
'amex' => ['name' => 'American Express', 'path' => __DIR__ . '/credit-cards/amex.png'],
|
||||
'argencard' => ['name' => 'Argencard', 'path' => __DIR__ . '/credit-cards/argencard.png'],
|
||||
'cabal' => ['name' => 'Cabal', 'path' => __DIR__ . '/credit-cards/cabal.png'],
|
||||
'censosud' => ['name' => 'Consosud', 'path' => __DIR__ . '/credit-cards/consosud.png'],
|
||||
'diners' => ['name' => 'Diners Club', 'path' => __DIR__ . '/credit-cards/diners.png'],
|
||||
'discover' => ['name' => 'Discover', 'path' => __DIR__ . '/credit-cards/discover.png'],
|
||||
'elo' => ['name' => 'Elo', 'path' => __DIR__ . '/credit-cards/elo.png'],
|
||||
'hipercard' => ['name' => 'Hipercard', 'path' => __DIR__ . '/credit-cards/hipercard.png'],
|
||||
'jcb' => ['name' => 'JCB', 'path' => __DIR__ . '/credit-cards/jcb.png'],
|
||||
'mastercard' => ['name' => 'Mastercard', 'path' => __DIR__ . '/credit-cards/mastercard.png'],
|
||||
'naranja' => ['name' => 'Naranja', 'path' => __DIR__ . '/credit-cards/naranja.png'],
|
||||
'targeta-shopping' => ['name' => 'Tarjeta Shopping', 'path' => __DIR__ . '/credit-cards/tarjeta-shopping.png'],
|
||||
'union-china-pay' => ['name' => 'Union China Pay', 'path' => __DIR__ . '/credit-cards/union-china-pay.png'],
|
||||
'visa' => ['name' => 'Visa', 'path' => __DIR__ . '/credit-cards/visa.png'],
|
||||
'mir' => ['name' => 'MIR', 'path' => __DIR__ . '/credit-cards/mir.png'],
|
||||
'maestro' => ['name' => 'Maestro', 'path' => __DIR__ . '/credit-cards/maestro.png']
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,198 +1,198 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'af' => __DIR__ . '/flags/af.png',
|
||||
'ao' => __DIR__ . '/flags/ao.png',
|
||||
'al' => __DIR__ . '/flags/al.png',
|
||||
'ad' => __DIR__ . '/flags/ad.png',
|
||||
'ae' => __DIR__ . '/flags/ae.png',
|
||||
'ar' => __DIR__ . '/flags/ar.png',
|
||||
'am' => __DIR__ . '/flags/am.png',
|
||||
'ag' => __DIR__ . '/flags/ag.png',
|
||||
'au' => __DIR__ . '/flags/au.png',
|
||||
'at' => __DIR__ . '/flags/at.png',
|
||||
'az' => __DIR__ . '/flags/az.png',
|
||||
'bi' => __DIR__ . '/flags/bi.png',
|
||||
'be' => __DIR__ . '/flags/be.png',
|
||||
'bj' => __DIR__ . '/flags/bj.png',
|
||||
'bf' => __DIR__ . '/flags/bf.png',
|
||||
'bd' => __DIR__ . '/flags/bd.png',
|
||||
'bg' => __DIR__ . '/flags/bg.png',
|
||||
'bh' => __DIR__ . '/flags/bh.png',
|
||||
'bs' => __DIR__ . '/flags/bs.png',
|
||||
'ba' => __DIR__ . '/flags/ba.png',
|
||||
'by' => __DIR__ . '/flags/by.png',
|
||||
'bz' => __DIR__ . '/flags/bz.png',
|
||||
'bo' => __DIR__ . '/flags/bo.png',
|
||||
'br' => __DIR__ . '/flags/br.png',
|
||||
'bb' => __DIR__ . '/flags/bb.png',
|
||||
'bn' => __DIR__ . '/flags/bn.png',
|
||||
'bt' => __DIR__ . '/flags/bt.png',
|
||||
'bw' => __DIR__ . '/flags/bw.png',
|
||||
'cf' => __DIR__ . '/flags/cf.png',
|
||||
'ca' => __DIR__ . '/flags/ca.png',
|
||||
'ch' => __DIR__ . '/flags/ch.png',
|
||||
'cl' => __DIR__ . '/flags/cl.png',
|
||||
'cn' => __DIR__ . '/flags/cn.png',
|
||||
'ci' => __DIR__ . '/flags/ci.png',
|
||||
'cm' => __DIR__ . '/flags/cm.png',
|
||||
'cd' => __DIR__ . '/flags/cd.png',
|
||||
'cg' => __DIR__ . '/flags/cg.png',
|
||||
'co' => __DIR__ . '/flags/co.png',
|
||||
'km' => __DIR__ . '/flags/km.png',
|
||||
'cv' => __DIR__ . '/flags/cv.png',
|
||||
'cr' => __DIR__ . '/flags/cr.png',
|
||||
'cu' => __DIR__ . '/flags/cu.png',
|
||||
'cy' => __DIR__ . '/flags/cy.png',
|
||||
'cz' => __DIR__ . '/flags/cz.png',
|
||||
'de' => __DIR__ . '/flags/de.png',
|
||||
'dj' => __DIR__ . '/flags/dj.png',
|
||||
'dm' => __DIR__ . '/flags/dm.png',
|
||||
'dk' => __DIR__ . '/flags/dk.png',
|
||||
'do' => __DIR__ . '/flags/do.png',
|
||||
'dz' => __DIR__ . '/flags/dz.png',
|
||||
'ec' => __DIR__ . '/flags/ec.png',
|
||||
'eg' => __DIR__ . '/flags/eg.png',
|
||||
'er' => __DIR__ . '/flags/er.png',
|
||||
'es' => __DIR__ . '/flags/es.png',
|
||||
'ee' => __DIR__ . '/flags/ee.png',
|
||||
'et' => __DIR__ . '/flags/et.png',
|
||||
'fi' => __DIR__ . '/flags/fi.png',
|
||||
'fj' => __DIR__ . '/flags/fj.png',
|
||||
'fr' => __DIR__ . '/flags/fr.png',
|
||||
'fm' => __DIR__ . '/flags/fm.png',
|
||||
'ga' => __DIR__ . '/flags/ga.png',
|
||||
'gb' => __DIR__ . '/flags/gb.png',
|
||||
'ge' => __DIR__ . '/flags/ge.png',
|
||||
'gh' => __DIR__ . '/flags/gh.png',
|
||||
'gn' => __DIR__ . '/flags/gn.png',
|
||||
'gm' => __DIR__ . '/flags/gm.png',
|
||||
'gw' => __DIR__ . '/flags/gw.png',
|
||||
'gq' => __DIR__ . '/flags/gq.png',
|
||||
'gr' => __DIR__ . '/flags/gr.png',
|
||||
'gd' => __DIR__ . '/flags/gd.png',
|
||||
'gt' => __DIR__ . '/flags/gt.png',
|
||||
'gy' => __DIR__ . '/flags/gy.png',
|
||||
'hn' => __DIR__ . '/flags/hn.png',
|
||||
'hr' => __DIR__ . '/flags/hr.png',
|
||||
'ht' => __DIR__ . '/flags/ht.png',
|
||||
'hu' => __DIR__ . '/flags/hu.png',
|
||||
'id' => __DIR__ . '/flags/id.png',
|
||||
'in' => __DIR__ . '/flags/in.png',
|
||||
'ie' => __DIR__ . '/flags/ie.png',
|
||||
'ir' => __DIR__ . '/flags/ir.png',
|
||||
'iq' => __DIR__ . '/flags/iq.png',
|
||||
'is' => __DIR__ . '/flags/is.png',
|
||||
'il' => __DIR__ . '/flags/il.png',
|
||||
'it' => __DIR__ . '/flags/it.png',
|
||||
'jm' => __DIR__ . '/flags/jm.png',
|
||||
'jo' => __DIR__ . '/flags/jo.png',
|
||||
'jp' => __DIR__ . '/flags/jp.png',
|
||||
'kz' => __DIR__ . '/flags/kz.png',
|
||||
'ke' => __DIR__ . '/flags/ke.png',
|
||||
'kg' => __DIR__ . '/flags/kg.png',
|
||||
'kh' => __DIR__ . '/flags/kh.png',
|
||||
'ki' => __DIR__ . '/flags/ki.png',
|
||||
'kn' => __DIR__ . '/flags/kn.png',
|
||||
'kr' => __DIR__ . '/flags/kr.png',
|
||||
'kw' => __DIR__ . '/flags/kw.png',
|
||||
'la' => __DIR__ . '/flags/la.png',
|
||||
'lb' => __DIR__ . '/flags/lb.png',
|
||||
'lr' => __DIR__ . '/flags/lr.png',
|
||||
'ly' => __DIR__ . '/flags/ly.png',
|
||||
'lc' => __DIR__ . '/flags/lc.png',
|
||||
'li' => __DIR__ . '/flags/li.png',
|
||||
'lk' => __DIR__ . '/flags/lk.png',
|
||||
'ls' => __DIR__ . '/flags/ls.png',
|
||||
'lt' => __DIR__ . '/flags/ls.png',
|
||||
'lu' => __DIR__ . '/flags/lu.png',
|
||||
'lv' => __DIR__ . '/flags/lv.png',
|
||||
'ma' => __DIR__ . '/flags/ma.png',
|
||||
'mc' => __DIR__ . '/flags/mc.png',
|
||||
'md' => __DIR__ . '/flags/md.png',
|
||||
'mg' => __DIR__ . '/flags/mg.png',
|
||||
'mv' => __DIR__ . '/flags/mv.png',
|
||||
'mx' => __DIR__ . '/flags/mx.png',
|
||||
'mh' => __DIR__ . '/flags/mh.png',
|
||||
'mk' => __DIR__ . '/flags/mk.png',
|
||||
'ml' => __DIR__ . '/flags/ml.png',
|
||||
'mt' => __DIR__ . '/flags/mt.png',
|
||||
'mm' => __DIR__ . '/flags/mm.png',
|
||||
'me' => __DIR__ . '/flags/me.png',
|
||||
'mn' => __DIR__ . '/flags/mn.png',
|
||||
'mz' => __DIR__ . '/flags/mz.png',
|
||||
'mr' => __DIR__ . '/flags/mr.png',
|
||||
'mu' => __DIR__ . '/flags/mu.png',
|
||||
'mw' => __DIR__ . '/flags/mw.png',
|
||||
'my' => __DIR__ . '/flags/my.png',
|
||||
'na' => __DIR__ . '/flags/na.png',
|
||||
'ne' => __DIR__ . '/flags/ne.png',
|
||||
'ng' => __DIR__ . '/flags/ng.png',
|
||||
'ni' => __DIR__ . '/flags/ni.png',
|
||||
'nl' => __DIR__ . '/flags/nl.png',
|
||||
'no' => __DIR__ . '/flags/no.png',
|
||||
'np' => __DIR__ . '/flags/np.png',
|
||||
'nr' => __DIR__ . '/flags/nr.png',
|
||||
'nz' => __DIR__ . '/flags/nz.png',
|
||||
'om' => __DIR__ . '/flags/om.png',
|
||||
'pk' => __DIR__ . '/flags/pk.png',
|
||||
'pa' => __DIR__ . '/flags/pa.png',
|
||||
'pe' => __DIR__ . '/flags/pe.png',
|
||||
'ph' => __DIR__ . '/flags/ph.png',
|
||||
'pw' => __DIR__ . '/flags/pw.png',
|
||||
'pg' => __DIR__ . '/flags/pg.png',
|
||||
'pl' => __DIR__ . '/flags/pl.png',
|
||||
'kp' => __DIR__ . '/flags/kp.png',
|
||||
'pt' => __DIR__ . '/flags/pt.png',
|
||||
'py' => __DIR__ . '/flags/py.png',
|
||||
'qa' => __DIR__ . '/flags/qa.png',
|
||||
'ro' => __DIR__ . '/flags/ro.png',
|
||||
'ru' => __DIR__ . '/flags/ru.png',
|
||||
'rw' => __DIR__ . '/flags/rw.png',
|
||||
'sa' => __DIR__ . '/flags/sa.png',
|
||||
'sd' => __DIR__ . '/flags/sd.png',
|
||||
'sn' => __DIR__ . '/flags/sn.png',
|
||||
'sg' => __DIR__ . '/flags/sg.png',
|
||||
'sb' => __DIR__ . '/flags/sb.png',
|
||||
'sl' => __DIR__ . '/flags/sl.png',
|
||||
'sv' => __DIR__ . '/flags/sv.png',
|
||||
'sm' => __DIR__ . '/flags/sm.png',
|
||||
'so' => __DIR__ . '/flags/so.png',
|
||||
'rs' => __DIR__ . '/flags/rs.png',
|
||||
'ss' => __DIR__ . '/flags/ss.png',
|
||||
'st' => __DIR__ . '/flags/st.png',
|
||||
'sr' => __DIR__ . '/flags/sr.png',
|
||||
'sk' => __DIR__ . '/flags/sk.png',
|
||||
'si' => __DIR__ . '/flags/si.png',
|
||||
'se' => __DIR__ . '/flags/se.png',
|
||||
'sz' => __DIR__ . '/flags/sz.png',
|
||||
'sc' => __DIR__ . '/flags/sc.png',
|
||||
'sy' => __DIR__ . '/flags/sy.png',
|
||||
'td' => __DIR__ . '/flags/td.png',
|
||||
'tg' => __DIR__ . '/flags/tg.png',
|
||||
'th' => __DIR__ . '/flags/th.png',
|
||||
'tj' => __DIR__ . '/flags/tj.png',
|
||||
'tm' => __DIR__ . '/flags/tm.png',
|
||||
'tl' => __DIR__ . '/flags/tl.png',
|
||||
'to' => __DIR__ . '/flags/to.png',
|
||||
'tt' => __DIR__ . '/flags/tt.png',
|
||||
'tn' => __DIR__ . '/flags/tn.png',
|
||||
'tr' => __DIR__ . '/flags/tr.png',
|
||||
'tv' => __DIR__ . '/flags/tv.png',
|
||||
'tz' => __DIR__ . '/flags/tz.png',
|
||||
'ug' => __DIR__ . '/flags/ug.png',
|
||||
'ua' => __DIR__ . '/flags/ua.png',
|
||||
'uy' => __DIR__ . '/flags/uy.png',
|
||||
'us' => __DIR__ . '/flags/us.png',
|
||||
'uz' => __DIR__ . '/flags/uz.png',
|
||||
'va' => __DIR__ . '/flags/va.png',
|
||||
'vc' => __DIR__ . '/flags/vc.png',
|
||||
've' => __DIR__ . '/flags/ve.png',
|
||||
'vn' => __DIR__ . '/flags/vn.png',
|
||||
'vu' => __DIR__ . '/flags/vu.png',
|
||||
'ws' => __DIR__ . '/flags/ws.png',
|
||||
'ye' => __DIR__ . '/flags/ye.png',
|
||||
'za' => __DIR__ . '/flags/za.png',
|
||||
'zm' => __DIR__ . '/flags/zm.png',
|
||||
'zw' => __DIR__ . '/flags/zw.png',
|
||||
'af' => ['name' => 'Afghanistan', 'path' => __DIR__ . '/flags/af.png'],
|
||||
'ao' => ['name' => 'Angola', 'path' => __DIR__ . '/flags/ao.png'],
|
||||
'al' => ['name' => 'Albania', 'path' => __DIR__ . '/flags/al.png'],
|
||||
'ad' => ['name' => 'Andorra', 'path' => __DIR__ . '/flags/ad.png'],
|
||||
'ae' => ['name' => 'United Arab Emirates', 'path' => __DIR__ . '/flags/ae.png'],
|
||||
'ar' => ['name' => 'Argentina', 'path' => __DIR__ . '/flags/ar.png'],
|
||||
'am' => ['name' => 'Armenia', 'path' => __DIR__ . '/flags/am.png'],
|
||||
'ag' => ['name' => 'Antigua and Barbuda', 'path' => __DIR__ . '/flags/ag.png'],
|
||||
'au' => ['name' => 'Australia', 'path' => __DIR__ . '/flags/au.png'],
|
||||
'at' => ['name' => 'Austria', 'path' => __DIR__ . '/flags/at.png'],
|
||||
'az' => ['name' => 'Azerbaijan', 'path' => __DIR__ . '/flags/az.png'],
|
||||
'bi' => ['name' => 'Burundi', 'path' => __DIR__ . '/flags/bi.png'],
|
||||
'be' => ['name' => 'Belgium', 'path' => __DIR__ . '/flags/be.png'],
|
||||
'bj' => ['name' => 'Benin', 'path' => __DIR__ . '/flags/bj.png'],
|
||||
'bf' => ['name' => 'Burkina Faso', 'path' => __DIR__ . '/flags/bf.png'],
|
||||
'bd' => ['name' => 'Bangladesh', 'path' => __DIR__ . '/flags/bd.png'],
|
||||
'bg' => ['name' => 'Bulgaria', 'path' => __DIR__ . '/flags/bg.png'],
|
||||
'bh' => ['name' => 'Bahrain', 'path' => __DIR__ . '/flags/bh.png'],
|
||||
'bs' => ['name' => 'Bahamas', 'path' => __DIR__ . '/flags/bs.png'],
|
||||
'ba' => ['name' => 'Bosnia and Herzegovina', 'path' => __DIR__ . '/flags/ba.png'],
|
||||
'by' => ['name' => 'Belarus', 'path' => __DIR__ . '/flags/by.png'],
|
||||
'bz' => ['name' => 'Belize', 'path' => __DIR__ . '/flags/bz.png'],
|
||||
'bo' => ['name' => 'Bolivia', 'path' => __DIR__ . '/flags/bo.png'],
|
||||
'br' => ['name' => 'Brazil', 'path' => __DIR__ . '/flags/br.png'],
|
||||
'bb' => ['name' => 'Barbados', 'path' => __DIR__ . '/flags/bb.png'],
|
||||
'bn' => ['name' => 'Brunei Darussalam', 'path' => __DIR__ . '/flags/bn.png'],
|
||||
'bt' => ['name' => 'Bhutan', 'path' => __DIR__ . '/flags/bt.png'],
|
||||
'bw' => ['name' => 'Botswana', 'path' => __DIR__ . '/flags/bw.png'],
|
||||
'cf' => ['name' => 'Central African Republic', 'path' => __DIR__ . '/flags/cf.png'],
|
||||
'ca' => ['name' => 'Canada', 'path' => __DIR__ . '/flags/ca.png'],
|
||||
'ch' => ['name' => 'Switzerland', 'path' => __DIR__ . '/flags/ch.png'],
|
||||
'cl' => ['name' => 'Chile', 'path' => __DIR__ . '/flags/cl.png'],
|
||||
'cn' => ['name' => 'China', 'path' => __DIR__ . '/flags/cn.png'],
|
||||
'ci' => ['name' => 'Côte d\'Ivoire', 'path' => __DIR__ . '/flags/ci.png'],
|
||||
'cm' => ['name' => 'Cameroon', 'path' => __DIR__ . '/flags/cm.png'],
|
||||
'cd' => ['name' => 'Democratic Republic of the Congo', 'path' => __DIR__ . '/flags/cd.png'],
|
||||
'cg' => ['name' => 'Republic of the Congo', 'path' => __DIR__ . '/flags/cg.png'],
|
||||
'co' => ['name' => 'Colombia', 'path' => __DIR__ . '/flags/co.png'],
|
||||
'km' => ['name' => 'Comoros', 'path' => __DIR__ . '/flags/km.png'],
|
||||
'cv' => ['name' => 'Cape Verde', 'path' => __DIR__ . '/flags/cv.png'],
|
||||
'cr' => ['name' => 'Costa Rica', 'path' => __DIR__ . '/flags/cr.png'],
|
||||
'cu' => ['name' => 'Cuba', 'path' => __DIR__ . '/flags/cu.png'],
|
||||
'cy' => ['name' => 'Cyprus', 'path' => __DIR__ . '/flags/cy.png'],
|
||||
'cz' => ['name' => 'Czech Republic', 'path' => __DIR__ . '/flags/cz.png'],
|
||||
'de' => ['name' => 'Germany', 'path' => __DIR__ . '/flags/de.png'],
|
||||
'dj' => ['name' => 'Djibouti', 'path' => __DIR__ . '/flags/dj.png'],
|
||||
'dm' => ['name' => 'Dominica', 'path' => __DIR__ . '/flags/dm.png'],
|
||||
'dk' => ['name' => 'Denmark', 'path' => __DIR__ . '/flags/dk.png'],
|
||||
'do' => ['name' => 'Dominican Republic', 'path' => __DIR__ . '/flags/do.png'],
|
||||
'dz' => ['name' => 'Algeria', 'path' => __DIR__ . '/flags/dz.png'],
|
||||
'ec' => ['name' => 'Ecuador', 'path' => __DIR__ . '/flags/ec.png'],
|
||||
'eg' => ['name' => 'Egypt', 'path' => __DIR__ . '/flags/eg.png'],
|
||||
'er' => ['name' => 'Eritrea', 'path' => __DIR__ . '/flags/er.png'],
|
||||
'es' => ['name' => 'Spain', 'path' => __DIR__ . '/flags/es.png'],
|
||||
'ee' => ['name' => 'Estonia', 'path' => __DIR__ . '/flags/ee.png'],
|
||||
'et' => ['name' => 'Ethiopia', 'path' => __DIR__ . '/flags/et.png'],
|
||||
'fi' => ['name' => 'Finland', 'path' => __DIR__ . '/flags/fi.png'],
|
||||
'fj' => ['name' => 'Fiji', 'path' => __DIR__ . '/flags/fj.png'],
|
||||
'fr' => ['name' => 'France', 'path' => __DIR__ . '/flags/fr.png'],
|
||||
'fm' => ['name' => 'Micronesia (Federated States of)', 'path' => __DIR__ . '/flags/fm.png'],
|
||||
'ga' => ['name' => 'Gabon', 'path' => __DIR__ . '/flags/ga.png'],
|
||||
'gb' => ['name' => 'United Kingdom', 'path' => __DIR__ . '/flags/gb.png'],
|
||||
'ge' => ['name' => 'Georgia', 'path' => __DIR__ . '/flags/ge.png'],
|
||||
'gh' => ['name' => 'Ghana', 'path' => __DIR__ . '/flags/gh.png'],
|
||||
'gn' => ['name' => 'Guinea', 'path' => __DIR__ . '/flags/gn.png'],
|
||||
'gm' => ['name' => 'Gambia', 'path' => __DIR__ . '/flags/gm.png'],
|
||||
'gw' => ['name' => 'Guinea-Bissau', 'path' => __DIR__ . '/flags/gw.png'],
|
||||
'gq' => ['name' => 'Equatorial Guinea', 'path' => __DIR__ . '/flags/gq.png'],
|
||||
'gr' => ['name' => 'Greece', 'path' => __DIR__ . '/flags/gr.png'],
|
||||
'gd' => ['name' => 'Grenada', 'path' => __DIR__ . '/flags/gd.png'],
|
||||
'gt' => ['name' => 'Guatemala', 'path' => __DIR__ . '/flags/gt.png'],
|
||||
'gy' => ['name' => 'Guyana', 'path' => __DIR__ . '/flags/gy.png'],
|
||||
'hn' => ['name' => 'Honduras', 'path' => __DIR__ . '/flags/hn.png'],
|
||||
'hr' => ['name' => 'Croatia', 'path' => __DIR__ . '/flags/hr.png'],
|
||||
'ht' => ['name' => 'Haiti', 'path' => __DIR__ . '/flags/ht.png'],
|
||||
'hu' => ['name' => 'Hungary', 'path' => __DIR__ . '/flags/hu.png'],
|
||||
'id' => ['name' => 'Indonesia', 'path' => __DIR__ . '/flags/id.png'],
|
||||
'in' => ['name' => 'India', 'path' => __DIR__ . '/flags/in.png'],
|
||||
'ie' => ['name' => 'Ireland', 'path' => __DIR__ . '/flags/ie.png'],
|
||||
'ir' => ['name' => 'Iran (Islamic Republic of)', 'path' => __DIR__ . '/flags/ir.png'],
|
||||
'iq' => ['name' => 'Iraq', 'path' => __DIR__ . '/flags/iq.png'],
|
||||
'is' => ['name' => 'Iceland', 'path' => __DIR__ . '/flags/is.png'],
|
||||
'il' => ['name' => 'Israel', 'path' => __DIR__ . '/flags/il.png'],
|
||||
'it' => ['name' => 'Italy', 'path' => __DIR__ . '/flags/it.png'],
|
||||
'jm' => ['name' => 'Jamaica', 'path' => __DIR__ . '/flags/jm.png'],
|
||||
'jo' => ['name' => 'Jordan', 'path' => __DIR__ . '/flags/jo.png'],
|
||||
'jp' => ['name' => 'Japan', 'path' => __DIR__ . '/flags/jp.png'],
|
||||
'kz' => ['name' => 'Kazakhstan', 'path' => __DIR__ . '/flags/kz.png'],
|
||||
'ke' => ['name' => 'Kenya', 'path' => __DIR__ . '/flags/ke.png'],
|
||||
'kg' => ['name' => 'Kyrgyzstan', 'path' => __DIR__ . '/flags/kg.png'],
|
||||
'kh' => ['name' => 'Cambodia', 'path' => __DIR__ . '/flags/kh.png'],
|
||||
'ki' => ['name' => 'Kiribati', 'path' => __DIR__ . '/flags/ki.png'],
|
||||
'kn' => ['name' => 'Saint Kitts and Nevis', 'path' => __DIR__ . '/flags/kn.png'],
|
||||
'kr' => ['name' => 'South Korea', 'path' => __DIR__ . '/flags/kr.png'],
|
||||
'kw' => ['name' => 'Kuwait', 'path' => __DIR__ . '/flags/kw.png'],
|
||||
'la' => ['name' => 'Lao People\'s Democratic Republic', 'path' => __DIR__ . '/flags/la.png'],
|
||||
'lb' => ['name' => 'Lebanon', 'path' => __DIR__ . '/flags/lb.png'],
|
||||
'lr' => ['name' => 'Liberia', 'path' => __DIR__ . '/flags/lr.png'],
|
||||
'ly' => ['name' => 'Libya', 'path' => __DIR__ . '/flags/ly.png'],
|
||||
'lc' => ['name' => 'Saint Lucia', 'path' => __DIR__ . '/flags/lc.png'],
|
||||
'li' => ['name' => 'Liechtenstein', 'path' => __DIR__ . '/flags/li.png'],
|
||||
'lk' => ['name' => 'Sri Lanka', 'path' => __DIR__ . '/flags/lk.png'],
|
||||
'ls' => ['name' => 'Lesotho', 'path' => __DIR__ . '/flags/ls.png'],
|
||||
'lt' => ['name' => 'Lithuania', 'path' => __DIR__ . '/flags/lt.png'],
|
||||
'lu' => ['name' => 'Luxembourg', 'path' => __DIR__ . '/flags/lu.png'],
|
||||
'lv' => ['name' => 'Latvia', 'path' => __DIR__ . '/flags/lv.png'],
|
||||
'ma' => ['name' => 'Morocco', 'path' => __DIR__ . '/flags/ma.png'],
|
||||
'mc' => ['name' => 'Monaco', 'path' => __DIR__ . '/flags/mc.png'],
|
||||
'md' => ['name' => 'Moldova', 'path' => __DIR__ . '/flags/md.png'],
|
||||
'mg' => ['name' => 'Madagascar', 'path' => __DIR__ . '/flags/mg.png'],
|
||||
'mv' => ['name' => 'Maldives', 'path' => __DIR__ . '/flags/mv.png'],
|
||||
'mx' => ['name' => 'Mexico', 'path' => __DIR__ . '/flags/mx.png'],
|
||||
'mh' => ['name' => 'Marshall Islands', 'path' => __DIR__ . '/flags/mh.png'],
|
||||
'mk' => ['name' => 'North Macedonia', 'path' => __DIR__ . '/flags/mk.png'],
|
||||
'ml' => ['name' => 'Mali', 'path' => __DIR__ . '/flags/ml.png'],
|
||||
'mt' => ['name' => 'Malta', 'path' => __DIR__ . '/flags/mt.png'],
|
||||
'mm' => ['name' => 'Myanmar', 'path' => __DIR__ . '/flags/mm.png'],
|
||||
'me' => ['name' => 'Montenegro', 'path' => __DIR__ . '/flags/me.png'],
|
||||
'mn' => ['name' => 'Mongolia', 'path' => __DIR__ . '/flags/mn.png'],
|
||||
'mz' => ['name' => 'Mozambique', 'path' => __DIR__ . '/flags/mz.png'],
|
||||
'mr' => ['name' => 'Mauritania', 'path' => __DIR__ . '/flags/mr.png'],
|
||||
'mu' => ['name' => 'Mauritius', 'path' => __DIR__ . '/flags/mu.png'],
|
||||
'mw' => ['name' => 'Malawi', 'path' => __DIR__ . '/flags/mw.png'],
|
||||
'my' => ['name' => 'Malaysia', 'path' => __DIR__ . '/flags/my.png'],
|
||||
'na' => ['name' => 'Namibia', 'path' => __DIR__ . '/flags/na.png'],
|
||||
'ne' => ['name' => 'Niger', 'path' => __DIR__ . '/flags/ne.png'],
|
||||
'ng' => ['name' => 'Nigeria', 'path' => __DIR__ . '/flags/ng.png'],
|
||||
'ni' => ['name' => 'Nicaragua', 'path' => __DIR__ . '/flags/ni.png'],
|
||||
'nl' => ['name' => 'Netherlands', 'path' => __DIR__ . '/flags/nl.png'],
|
||||
'no' => ['name' => 'Norway', 'path' => __DIR__ . '/flags/no.png'],
|
||||
'np' => ['name' => 'Nepal', 'path' => __DIR__ . '/flags/np.png'],
|
||||
'nr' => ['name' => 'Nauru', 'path' => __DIR__ . '/flags/nr.png'],
|
||||
'nz' => ['name' => 'New Zealand', 'path' => __DIR__ . '/flags/nz.png'],
|
||||
'om' => ['name' => 'Oman', 'path' => __DIR__ . '/flags/om.png'],
|
||||
'pk' => ['name' => 'Pakistan', 'path' => __DIR__ . '/flags/pk.png'],
|
||||
'pa' => ['name' => 'Panama', 'path' => __DIR__ . '/flags/pa.png'],
|
||||
'pe' => ['name' => 'Peru', 'path' => __DIR__ . '/flags/pe.png'],
|
||||
'ph' => ['name' => 'Philippines', 'path' => __DIR__ . '/flags/ph.png'],
|
||||
'pw' => ['name' => 'Palau', 'path' => __DIR__ . '/flags/pw.png'],
|
||||
'pg' => ['name' => 'Papua New Guinea', 'path' => __DIR__ . '/flags/pg.png'],
|
||||
'pl' => ['name' => 'Poland', 'path' => __DIR__ . '/flags/pl.png'],
|
||||
'kp' => ['name' => 'North Korea', 'path' => __DIR__ . '/flags/kp.png'],
|
||||
'pt' => ['name' => 'Portugal', 'path' => __DIR__ . '/flags/pt.png'],
|
||||
'py' => ['name' => 'Paraguay', 'path' => __DIR__ . '/flags/py.png'],
|
||||
'qa' => ['name' => 'Qatar', 'path' => __DIR__ . '/flags/qa.png'],
|
||||
'ro' => ['name' => 'Romania', 'path' => __DIR__ . '/flags/ro.png'],
|
||||
'ru' => ['name' => 'Russia', 'path' => __DIR__ . '/flags/ru.png'],
|
||||
'rw' => ['name' => 'Rwanda', 'path' => __DIR__ . '/flags/rw.png'],
|
||||
'sa' => ['name' => 'Saudi Arabia', 'path' => __DIR__ . '/flags/sa.png'],
|
||||
'sd' => ['name' => 'Sudan', 'path' => __DIR__ . '/flags/sd.png'],
|
||||
'sn' => ['name' => 'Senegal', 'path' => __DIR__ . '/flags/sn.png'],
|
||||
'sg' => ['name' => 'Singapore', 'path' => __DIR__ . '/flags/sg.png'],
|
||||
'sb' => ['name' => 'Solomon Islands', 'path' => __DIR__ . '/flags/sb.png'],
|
||||
'sl' => ['name' => 'Sierra Leone', 'path' => __DIR__ . '/flags/sl.png'],
|
||||
'sv' => ['name' => 'El Salvador', 'path' => __DIR__ . '/flags/sv.png'],
|
||||
'sm' => ['name' => 'San Marino', 'path' => __DIR__ . '/flags/sm.png'],
|
||||
'so' => ['name' => 'Somalia', 'path' => __DIR__ . '/flags/so.png'],
|
||||
'rs' => ['name' => 'Serbia', 'path' => __DIR__ . '/flags/rs.png'],
|
||||
'ss' => ['name' => 'South Sudan', 'path' => __DIR__ . '/flags/ss.png'],
|
||||
'st' => ['name' => 'Sao Tome and Principe', 'path' => __DIR__ . '/flags/st.png'],
|
||||
'sr' => ['name' => 'Suriname', 'path' => __DIR__ . '/flags/sr.png'],
|
||||
'sk' => ['name' => 'Slovakia', 'path' => __DIR__ . '/flags/sk.png'],
|
||||
'si' => ['name' => 'Slovenia', 'path' => __DIR__ . '/flags/si.png'],
|
||||
'se' => ['name' => 'Sweden', 'path' => __DIR__ . '/flags/se.png'],
|
||||
'sz' => ['name' => 'Eswatini', 'path' => __DIR__ . '/flags/sz.png'],
|
||||
'sc' => ['name' => 'Seychelles', 'path' => __DIR__ . '/flags/sc.png'],
|
||||
'sy' => ['name' => 'Syria', 'path' => __DIR__ . '/flags/sy.png'],
|
||||
'td' => ['name' => 'Chad', 'path' => __DIR__ . '/flags/td.png'],
|
||||
'tg' => ['name' => 'Togo', 'path' => __DIR__ . '/flags/tg.png'],
|
||||
'th' => ['name' => 'Thailand', 'path' => __DIR__ . '/flags/th.png'],
|
||||
'tj' => ['name' => 'Tajikistan', 'path' => __DIR__ . '/flags/tj.png'],
|
||||
'tm' => ['name' => 'Turkmenistan', 'path' => __DIR__ . '/flags/tm.png'],
|
||||
'tl' => ['name' => 'Timor-Leste', 'path' => __DIR__ . '/flags/tl.png'],
|
||||
'to' => ['name' => 'Tonga', 'path' => __DIR__ . '/flags/to.png'],
|
||||
'tt' => ['name' => 'Trinidad and Tobago', 'path' => __DIR__ . '/flags/tt.png'],
|
||||
'tn' => ['name' => 'Tunisia', 'path' => __DIR__ . '/flags/tn.png'],
|
||||
'tr' => ['name' => 'Turkey', 'path' => __DIR__ . '/flags/tr.png'],
|
||||
'tv' => ['name' => 'Tuvalu', 'path' => __DIR__ . '/flags/tv.png'],
|
||||
'tz' => ['name' => 'Tanzania', 'path' => __DIR__ . '/flags/tz.png'],
|
||||
'ug' => ['name' => 'Uganda', 'path' => __DIR__ . '/flags/ug.png'],
|
||||
'ua' => ['name' => 'Ukraine', 'path' => __DIR__ . '/flags/ua.png'],
|
||||
'uy' => ['name' => 'Uruguay', 'path' => __DIR__ . '/flags/uy.png'],
|
||||
'us' => ['name' => 'United States', 'path' => __DIR__ . '/flags/us.png'],
|
||||
'uz' => ['name' => 'Uzbekistan', 'path' => __DIR__ . '/flags/uz.png'],
|
||||
'va' => ['name' => 'Vatican City', 'path' => __DIR__ . '/flags/va.png'],
|
||||
'vc' => ['name' => 'Saint Vincent and the Grenadines', 'path' => __DIR__ . '/flags/vc.png'],
|
||||
've' => ['name' => 'Venezuela', 'path' => __DIR__ . '/flags/ve.png'],
|
||||
'vn' => ['name' => 'Vietnam', 'path' => __DIR__ . '/flags/vn.png'],
|
||||
'vu' => ['name' => 'Vanuatu', 'path' => __DIR__ . '/flags/vu.png'],
|
||||
'ws' => ['name' => 'Samoa', 'path' => __DIR__ . '/flags/ws.png'],
|
||||
'ye' => ['name' => 'Yemen', 'path' => __DIR__ . '/flags/ye.png'],
|
||||
'za' => ['name' => 'South Africa', 'path' => __DIR__ . '/flags/za.png'],
|
||||
'zm' => ['name' => 'Zambia', 'path' => __DIR__ . '/flags/zm.png'],
|
||||
'zw' => ['name' => 'Zimbabwe', 'path' => __DIR__ . '/flags/zw.png'],
|
||||
];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -88,11 +88,21 @@ return [
|
|||
'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.',
|
||||
'code' => 500,
|
||||
],
|
||||
Exception::GENERAL_CODES_DISABLED => [
|
||||
'name' => Exception::GENERAL_CODES_DISABLED,
|
||||
'description' => 'Invitation codes are disabled on this server. Please contact the server administrator.',
|
||||
'code' => 500,
|
||||
],
|
||||
Exception::GENERAL_USAGE_DISABLED => [
|
||||
'name' => Exception::GENERAL_USAGE_DISABLED,
|
||||
'description' => 'Usage stats is not configured. Please check the value of the _APP_USAGE_STATS environment variable of your Appwrite server.',
|
||||
'code' => 501,
|
||||
],
|
||||
Exception::GENERAL_NOT_IMPLEMENTED => [
|
||||
'name' => Exception::GENERAL_NOT_IMPLEMENTED,
|
||||
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
|
||||
'code' => 405,
|
||||
],
|
||||
|
||||
/** User Errors */
|
||||
Exception::USER_COUNT_EXCEEDED => [
|
||||
|
|
@ -107,12 +117,12 @@ return [
|
|||
],
|
||||
Exception::USER_ALREADY_EXISTS => [
|
||||
'name' => Exception::USER_ALREADY_EXISTS,
|
||||
'description' => 'A user with the same id, email, or phone already exists in your project.',
|
||||
'description' => 'A user with the same id, email, or phone already exists in this project.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::USER_BLOCKED => [
|
||||
'name' => Exception::USER_BLOCKED,
|
||||
'description' => 'The current user has been blocked.',
|
||||
'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_INVALID_TOKEN => [
|
||||
|
|
@ -130,6 +140,11 @@ return [
|
|||
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_INVALID_CODE => [
|
||||
'name' => Exception::USER_INVALID_CODE,
|
||||
'description' => 'The specified code is not valid. Contact your administrator for more information.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_IP_NOT_WHITELISTED => [
|
||||
'name' => Exception::USER_IP_NOT_WHITELISTED,
|
||||
'description' => 'Console registration is restricted to specific IPs. Contact your administrator for more information.',
|
||||
|
|
@ -165,11 +180,26 @@ return [
|
|||
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_RECENTLY_USED => [
|
||||
'name' => Exception::USER_PASSWORD_RECENTLY_USED,
|
||||
'description' => 'The password you are trying to use is similar to your previous password. For your security, please choose a different password and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_PERSONAL_DATA => [
|
||||
'name' => Exception::USER_PASSWORD_PERSONAL_DATA,
|
||||
'description' => 'The password you are trying to use contains references to your name, email, phone or userID. For your security, please choose a different password and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_SESSION_NOT_FOUND => [
|
||||
'name' => Exception::USER_SESSION_NOT_FOUND,
|
||||
'description' => 'The current user session could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::USER_IDENTITY_NOT_FOUND => [
|
||||
'name' => Exception::USER_IDENTITY_NOT_FOUND,
|
||||
'description' => 'The identity could not be found. Please sign in with OAuth provider to create identity first.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::USER_UNAUTHORIZED => [
|
||||
'name' => Exception::USER_UNAUTHORIZED,
|
||||
'description' => 'The current user is not authorized to perform the requested action.',
|
||||
|
|
@ -195,6 +225,21 @@ return [
|
|||
'description' => 'Missing ID from OAuth2 provider.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_OAUTH2_BAD_REQUEST => [
|
||||
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
|
||||
'description' => 'OAuth2 provider rejected the bad request.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_OAUTH2_UNAUTHORIZED => [
|
||||
'name' => Exception::USER_OAUTH2_UNAUTHORIZED,
|
||||
'description' => 'OAuth2 provider rejected the unauthorized request.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_OAUTH2_PROVIDER_ERROR => [
|
||||
'name' => Exception::USER_OAUTH2_PROVIDER_ERROR,
|
||||
'description' => 'OAuth2 provider returned some error.',
|
||||
'code' => 424,
|
||||
],
|
||||
|
||||
/** Teams */
|
||||
Exception::TEAM_NOT_FOUND => [
|
||||
|
|
@ -214,7 +259,7 @@ return [
|
|||
],
|
||||
Exception::TEAM_INVALID_SECRET => [
|
||||
'name' => Exception::TEAM_INVALID_SECRET,
|
||||
'description' => 'The team invitation secret is invalid.',
|
||||
'description' => 'The team invitation secret is invalid. Please request a new invitation and try again.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::TEAM_MEMBERSHIP_MISMATCH => [
|
||||
|
|
@ -229,7 +274,7 @@ return [
|
|||
],
|
||||
Exception::TEAM_ALREADY_EXISTS => [
|
||||
'name' => Exception::TEAM_ALREADY_EXISTS,
|
||||
'description' => 'Team with requested ID already exists.',
|
||||
'description' => 'Team with requested ID already exists. Please choose a different ID and try again.',
|
||||
'code' => 409,
|
||||
],
|
||||
|
||||
|
|
@ -241,7 +286,7 @@ return [
|
|||
],
|
||||
Exception::MEMBERSHIP_ALREADY_CONFIRMED => [
|
||||
'name' => Exception::MEMBERSHIP_ALREADY_CONFIRMED,
|
||||
'description' => 'Membership already confirmed',
|
||||
'description' => 'Membership is already confirmed.',
|
||||
'code' => 409,
|
||||
],
|
||||
|
||||
|
|
@ -273,6 +318,11 @@ return [
|
|||
],
|
||||
|
||||
/** Storage */
|
||||
Exception::STORAGE_FILE_ALREADY_EXISTS => [
|
||||
'name' => Exception::STORAGE_FILE_ALREADY_EXISTS,
|
||||
'description' => 'A storage file with the requested ID already exists.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::STORAGE_FILE_NOT_FOUND => [
|
||||
'name' => Exception::STORAGE_FILE_NOT_FOUND,
|
||||
'description' => 'The requested file could not be found.',
|
||||
|
|
@ -305,7 +355,7 @@ return [
|
|||
],
|
||||
Exception::STORAGE_BUCKET_ALREADY_EXISTS => [
|
||||
'name' => Exception::STORAGE_BUCKET_ALREADY_EXISTS,
|
||||
'description' => 'A storage bucket with the requested ID already exists.',
|
||||
'description' => 'A storage bucket with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::STORAGE_BUCKET_NOT_FOUND => [
|
||||
|
|
@ -325,7 +375,34 @@ return [
|
|||
],
|
||||
Exception::STORAGE_INVALID_APPWRITE_ID => [
|
||||
'name' => Exception::STORAGE_INVALID_APPWRITE_ID,
|
||||
'description' => 'The value for x-appwrite-id header is invalid. Please check the value of the x-appwrite-id header is valid id and not unique().',
|
||||
'description' => 'The value for x-appwrite-id header is invalid. Please check the value of the x-appwrite-id header is a valid id and not unique().',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** VCS */
|
||||
Exception::INSTALLATION_NOT_FOUND => [
|
||||
'name' => Exception::INSTALLATION_NOT_FOUND,
|
||||
'description' => 'Installation with the requested ID could not be found. Check to see if the ID is correct, or create the installation.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::PROVIDER_REPOSITORY_NOT_FOUND => [
|
||||
'name' => Exception::PROVIDER_REPOSITORY_NOT_FOUND,
|
||||
'description' => 'VCS (Version Control System) repository with the requested ID could not be found. Check to see if the ID is correct, and if it belongs to installationId you provided.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::REPOSITORY_NOT_FOUND => [
|
||||
'name' => Exception::REPOSITORY_NOT_FOUND,
|
||||
'description' => 'Repository with the requested ID could not be found. Check to see if the ID is correct, or create the respository.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::PROVIDER_CONTRIBUTION_CONFLICT => [
|
||||
'name' => Exception::PROVIDER_CONTRIBUTION_CONFLICT,
|
||||
'description' => 'External contribution is already authorized.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::GENERAL_PROVIDER_FAILURE => [
|
||||
'name' => Exception::GENERAL_PROVIDER_FAILURE,
|
||||
'description' => 'VCS (Version Control System) provider failed to proccess the request. We believe this is an error with the VCS provider. Try again, or contact support for more information.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
|
|
@ -340,6 +417,11 @@ return [
|
|||
'description' => 'The requested runtime is either inactive or unsupported. Please check the value of the _APP_FUNCTIONS_RUNTIMES environment variable.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::FUNCTION_ENTRYPOINT_MISSING => [
|
||||
'name' => Exception::FUNCTION_RUNTIME_UNSUPPORTED,
|
||||
'description' => 'Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
/** Builds */
|
||||
Exception::BUILD_NOT_FOUND => [
|
||||
|
|
@ -393,7 +475,7 @@ return [
|
|||
],
|
||||
Exception::COLLECTION_ALREADY_EXISTS => [
|
||||
'name' => Exception::COLLECTION_ALREADY_EXISTS,
|
||||
'description' => 'A collection with the requested ID already exists.',
|
||||
'description' => 'A collection with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::COLLECTION_LIMIT_EXCEEDED => [
|
||||
|
|
@ -415,17 +497,17 @@ return [
|
|||
],
|
||||
Exception::DOCUMENT_MISSING_DATA => [
|
||||
'name' => Exception::DOCUMENT_MISSING_DATA,
|
||||
'description' => 'The document data is missing. You must provide the document data.',
|
||||
'description' => 'The document data is missing. Try again with document data populated',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::DOCUMENT_MISSING_PAYLOAD => [
|
||||
'name' => Exception::DOCUMENT_MISSING_PAYLOAD,
|
||||
'description' => 'The document data and permissions are missing. You must provide either the document data or permissions to be updated.',
|
||||
'description' => 'The document data and permissions are missing. You must provide either document data or permissions to be updated.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::DOCUMENT_ALREADY_EXISTS => [
|
||||
'name' => Exception::DOCUMENT_ALREADY_EXISTS,
|
||||
'description' => 'Document with the requested ID already exists.',
|
||||
'description' => 'Document with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::DOCUMENT_UPDATE_CONFLICT => [
|
||||
|
|
@ -467,7 +549,7 @@ return [
|
|||
],
|
||||
Exception::ATTRIBUTE_ALREADY_EXISTS => [
|
||||
'name' => Exception::ATTRIBUTE_ALREADY_EXISTS,
|
||||
'description' => 'Attribute with the requested ID already exists.',
|
||||
'description' => 'Attribute with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
|
||||
|
|
@ -499,9 +581,14 @@ return [
|
|||
],
|
||||
Exception::INDEX_ALREADY_EXISTS => [
|
||||
'name' => Exception::INDEX_ALREADY_EXISTS,
|
||||
'description' => 'Index with the requested ID already exists.',
|
||||
'description' => 'Index with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::INDEX_INVALID => [
|
||||
'name' => Exception::INDEX_INVALID,
|
||||
'description' => 'Index invalid.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Project Errors */
|
||||
Exception::PROJECT_NOT_FOUND => [
|
||||
|
|
@ -511,7 +598,7 @@ return [
|
|||
],
|
||||
Exception::PROJECT_ALREADY_EXISTS => [
|
||||
'name' => Exception::PROJECT_ALREADY_EXISTS,
|
||||
'description' => 'Project with the requested ID already exists.',
|
||||
'description' => 'Project with the requested ID already exists. Try again with a different ID or use "unique()" to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::PROJECT_UNKNOWN => [
|
||||
|
|
@ -549,6 +636,46 @@ return [
|
|||
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::ROUTER_HOST_NOT_FOUND => [
|
||||
'name' => Exception::ROUTER_HOST_NOT_FOUND,
|
||||
'description' => 'Host is not trusted. This could occur because you have not configured a custom domain. Add a custom domain to your project first and try again.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::ROUTER_DOMAIN_NOT_CONFIGURED => [
|
||||
'name' => Exception::ROUTER_DOMAIN_NOT_CONFIGURED,
|
||||
'description' => 'Domain environment variables not configured. Please configure domain environment variables before using Appwrite outside of localhost.',
|
||||
'code' => 500,
|
||||
],
|
||||
Exception::RULE_RESOURCE_NOT_FOUND => [
|
||||
'name' => Exception::RULE_RESOURCE_NOT_FOUND,
|
||||
'description' => 'Resource could not be found. Please check if the resourceId and resourceType are correct, or if the resource actually exists.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::RULE_NOT_FOUND => [
|
||||
'name' => Exception::RULE_NOT_FOUND,
|
||||
'description' => 'Rule with the requested ID could not be found. Please check if the ID provided is correct or if the rule actually exists.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::RULE_ALREADY_EXISTS => [
|
||||
'name' => Exception::RULE_ALREADY_EXISTS,
|
||||
'description' => 'Domain is already used. Please try again with a different domain.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::RULE_VERIFICATION_FAILED => [
|
||||
'name' => Exception::RULE_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::PROJECT_SMTP_CONFIG_INVALID => [
|
||||
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
|
||||
'description' => 'Provided SMTP config is invalid. Please check the configured values and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::PROJECT_TEMPLATE_DEFAULT_DELETION => [
|
||||
'name' => Exception::PROJECT_TEMPLATE_DEFAULT_DELETION,
|
||||
'description' => 'You can\'t delete default template. If you are trying to reset your template changes, you can ignore this error as it\'s already been reset.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::WEBHOOK_NOT_FOUND => [
|
||||
'name' => Exception::WEBHOOK_NOT_FOUND,
|
||||
'description' => 'Webhook with the requested ID could not be found.',
|
||||
|
|
@ -564,16 +691,6 @@ return [
|
|||
'description' => 'Platform with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::DOMAIN_NOT_FOUND => [
|
||||
'name' => Exception::DOMAIN_NOT_FOUND,
|
||||
'description' => 'Domain with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::DOMAIN_ALREADY_EXISTS => [
|
||||
'name' => Exception::DOMAIN_ALREADY_EXISTS,
|
||||
'description' => 'A Domain with the requested ID already exists.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::VARIABLE_NOT_FOUND => [
|
||||
'name' => Exception::VARIABLE_NOT_FOUND,
|
||||
'description' => 'Variable with the requested ID could not be found.',
|
||||
|
|
@ -581,19 +698,9 @@ return [
|
|||
],
|
||||
Exception::VARIABLE_ALREADY_EXISTS => [
|
||||
'name' => Exception::VARIABLE_ALREADY_EXISTS,
|
||||
'description' => 'Variable with the same ID already exists in your project.',
|
||||
'description' => 'Variable with the same ID already exists in this project. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::DOMAIN_VERIFICATION_FAILED => [
|
||||
'name' => Exception::DOMAIN_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification for the requested domain has failed.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::DOMAIN_TARGET_INVALID => [
|
||||
'name' => Exception::DOMAIN_TARGET_INVALID,
|
||||
'description' => 'Your Appwrite instance is not publicly accessible. Please check the _APP_DOMAIN_TARGET environment variable of your Appwrite server.',
|
||||
'code' => 501,
|
||||
],
|
||||
Exception::GRAPHQL_NO_QUERY => [
|
||||
'name' => Exception::GRAPHQL_NO_QUERY,
|
||||
'description' => 'Param "query" is not optional.',
|
||||
|
|
@ -604,4 +711,21 @@ return [
|
|||
'description' => 'Too many queries.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Migrations */
|
||||
Exception::MIGRATION_NOT_FOUND => [
|
||||
'name' => Exception::MIGRATION_NOT_FOUND,
|
||||
'description' => 'Migration with the requested ID could not be found. Please verify that the provided ID is correct and try again.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::MIGRATION_ALREADY_EXISTS => [
|
||||
'name' => Exception::MIGRATION_ALREADY_EXISTS,
|
||||
'description' => 'Migration with the requested ID already exists. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MIGRATION_IN_PROGRESS => [
|
||||
'name' => Exception::MIGRATION_IN_PROGRESS,
|
||||
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
|
||||
'code' => 409,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -236,5 +236,19 @@ return [
|
|||
'update' => [
|
||||
'$description' => 'This event triggers when a function is updated.',
|
||||
]
|
||||
],
|
||||
'rules' => [
|
||||
'$model' => Response::MODEL_PROXY_RULE,
|
||||
'$resource' => true,
|
||||
'$description' => 'This event triggers on any proxy rule event.',
|
||||
'create' => [
|
||||
'$description' => 'This event triggers when a proxy rule is created.'
|
||||
],
|
||||
'delete' => [
|
||||
'$description' => 'This event triggers when a proxy rule is deleted.',
|
||||
],
|
||||
'update' => [
|
||||
'$description' => 'This event triggers when a proxy rule is updated.',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,76 +1,537 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* ISO 639-1 standard language codes
|
||||
* https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
*
|
||||
* Source:
|
||||
* https://www.andiamo.co.uk/resources/iso-language-codes/
|
||||
*
|
||||
*/
|
||||
|
||||
return [
|
||||
'af', // Afrikaans
|
||||
'ar', // Arabic
|
||||
'as', // Assamese
|
||||
'az', // Azerbaijani
|
||||
'be', // Belarusian
|
||||
'bg', // Bulgarian
|
||||
'bh', // Bihari
|
||||
'bn', // Bengali
|
||||
'bs', // Bosnian
|
||||
'ca', // Catalan
|
||||
'cs', // Czech
|
||||
'da', // Danish
|
||||
'de', // German
|
||||
'en', // English
|
||||
'eo', // Esperanto
|
||||
'es', // Spanish
|
||||
'fa', // Farsi/Persian
|
||||
'fi', // Finnish
|
||||
'fo', // Faroese
|
||||
'fr', // French
|
||||
'el', // Greek
|
||||
'ga', // Irish
|
||||
'gu', // Gujrati
|
||||
'he', // Hebrew
|
||||
'hi', // Hindi,
|
||||
'hr', // Croatian
|
||||
'hu', // Hungarian
|
||||
'hy', // Armenian
|
||||
'id', // Indonesian
|
||||
'is', // Icelandic
|
||||
'it', // Italian
|
||||
'ja', // Japanese
|
||||
'jv', // Javanese
|
||||
'kn', // Kannada
|
||||
'km', // Khmer
|
||||
'ko', // Korean
|
||||
'la', // Latin
|
||||
'lb', // Luxembourgish
|
||||
'lt', // Lithuanian
|
||||
'lv', // Latvian
|
||||
'ml', // Malayalam
|
||||
'mr', // Marathi
|
||||
'ms', // Malay
|
||||
'nb', // Norwegian bokmål
|
||||
'nl', // Dutch
|
||||
'nn', // Norwegian nynorsk
|
||||
'ne', // Nepali
|
||||
'or', // Oriya
|
||||
'tl', // Filipino
|
||||
'pl', // Polish
|
||||
'pt-br', // Portuguese - Brazil
|
||||
'pt-pt', // Portuguese - Portugal
|
||||
'pa', // Punjabi
|
||||
'ro', // Romanian
|
||||
'ru', // Russian
|
||||
'sa', //Sanskrit
|
||||
'sd', // Sindhi
|
||||
'si', // Sinhala
|
||||
'sk', // Slovakia
|
||||
'sl', // Slovenian
|
||||
'sn', // Shona
|
||||
'sq', // Albanian
|
||||
'sv', // Swedish
|
||||
'ta', // Tamil
|
||||
'te', // Telugu
|
||||
'th', // Thai
|
||||
'tr', // Turkish
|
||||
'uk', // Ukrainian
|
||||
'ur', // Urdu
|
||||
'vi', // Vietnamese
|
||||
'zh-cn', // Chinese - China
|
||||
'zh-tw', // Chinese - Taiwan
|
||||
[
|
||||
"code" => "af",
|
||||
"name" => "Afrikaans",
|
||||
],
|
||||
[
|
||||
"code" => "ar-ae",
|
||||
"name" => "Arabic (U.A.E.)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-bh",
|
||||
"name" => "Arabic (Bahrain)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-dz",
|
||||
"name" => "Arabic (Algeria)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-eg",
|
||||
"name" => "Arabic (Egypt)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-iq",
|
||||
"name" => "Arabic (Iraq)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-jo",
|
||||
"name" => "Arabic (Jordan)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-kw",
|
||||
"name" => "Arabic (Kuwait)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-lb",
|
||||
"name" => "Arabic (Lebanon)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-ly",
|
||||
"name" => "Arabic (Libya)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-ma",
|
||||
"name" => "Arabic (Morocco)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-om",
|
||||
"name" => "Arabic (Oman)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-qa",
|
||||
"name" => "Arabic (Qatar)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-sa",
|
||||
"name" => "Arabic (Saudi Arabia)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-sy",
|
||||
"name" => "Arabic (Syria)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-tn",
|
||||
"name" => "Arabic (Tunisia)",
|
||||
],
|
||||
[
|
||||
"code" => "ar-ye",
|
||||
"name" => "Arabic (Yemen)",
|
||||
],
|
||||
[
|
||||
"code" => "as",
|
||||
"name" => "Assamese",
|
||||
],
|
||||
[
|
||||
"code" => "az",
|
||||
"name" => "Azerbaijani",
|
||||
],
|
||||
[
|
||||
"code" => "be",
|
||||
"name" => "Belarusian",
|
||||
],
|
||||
[
|
||||
"code" => "bg",
|
||||
"name" => "Bulgarian",
|
||||
],
|
||||
[
|
||||
"code" => "bh",
|
||||
"name" => "Bihari",
|
||||
],
|
||||
[
|
||||
"code" => "bn",
|
||||
"name" => "Bengali",
|
||||
],
|
||||
[
|
||||
"code" => "bs",
|
||||
"name" => "Bosnian",
|
||||
],
|
||||
[
|
||||
"code" => "ca",
|
||||
"name" => "Catalan",
|
||||
],
|
||||
[
|
||||
"code" => "cs",
|
||||
"name" => "Czech",
|
||||
],
|
||||
[
|
||||
"code" => "cy",
|
||||
"name" => "Welsh",
|
||||
],
|
||||
[
|
||||
"code" => "da",
|
||||
"name" => "Danish",
|
||||
],
|
||||
[
|
||||
"code" => "de",
|
||||
"name" => "German (Standard)",
|
||||
],
|
||||
[
|
||||
"code" => "de-at",
|
||||
"name" => "German (Austria)",
|
||||
],
|
||||
[
|
||||
"code" => "de-ch",
|
||||
"name" => "German (Switzerland)",
|
||||
],
|
||||
[
|
||||
"code" => "de-li",
|
||||
"name" => "German (Liechtenstein)",
|
||||
],
|
||||
[
|
||||
"code" => "de-lu",
|
||||
"name" => "German (Luxembourg)",
|
||||
],
|
||||
[
|
||||
"code" => "el",
|
||||
"name" => "Greek",
|
||||
],
|
||||
[
|
||||
"code" => "en",
|
||||
"name" => "English",
|
||||
],
|
||||
[
|
||||
"code" => "en-au",
|
||||
"name" => "English (Australia)",
|
||||
],
|
||||
[
|
||||
"code" => "en-bz",
|
||||
"name" => "English (Belize)",
|
||||
],
|
||||
[
|
||||
"code" => "en-ca",
|
||||
"name" => "English (Canada)",
|
||||
],
|
||||
[
|
||||
"code" => "en-gb",
|
||||
"name" => "English (United Kingdom)",
|
||||
],
|
||||
[
|
||||
"code" => "en-ie",
|
||||
"name" => "English (Ireland)",
|
||||
],
|
||||
[
|
||||
"code" => "en-jm",
|
||||
"name" => "English (Jamaica)",
|
||||
],
|
||||
[
|
||||
"code" => "en-nz",
|
||||
"name" => "English (New Zealand)",
|
||||
],
|
||||
[
|
||||
"code" => "en-tt",
|
||||
"name" => "English (Trinidad)",
|
||||
],
|
||||
[
|
||||
"code" => "en-us",
|
||||
"name" => "English (United States)",
|
||||
],
|
||||
[
|
||||
"code" => "en-za",
|
||||
"name" => "English (South Africa)",
|
||||
],
|
||||
[
|
||||
"code" => "eo",
|
||||
"name" => "Esperanto",
|
||||
],
|
||||
[
|
||||
"code" => "es",
|
||||
"name" => "Spanish (Spain)",
|
||||
],
|
||||
[
|
||||
"code" => "es-ar",
|
||||
"name" => "Spanish (Argentina)",
|
||||
],
|
||||
[
|
||||
"code" => "es-bo",
|
||||
"name" => "Spanish (Bolivia)",
|
||||
],
|
||||
[
|
||||
"code" => "es-cl",
|
||||
"name" => "Spanish (Chile)",
|
||||
],
|
||||
[
|
||||
"code" => "es-co",
|
||||
"name" => "Spanish (Colombia)",
|
||||
],
|
||||
[
|
||||
"code" => "es-cr",
|
||||
"name" => "Spanish (Costa Rica)",
|
||||
],
|
||||
[
|
||||
"code" => "es-do",
|
||||
"name" => "Spanish (Dominican Republic)",
|
||||
],
|
||||
[
|
||||
"code" => "es-ec",
|
||||
"name" => "Spanish (Ecuador)",
|
||||
],
|
||||
[
|
||||
"code" => "es-gt",
|
||||
"name" => "Spanish (Guatemala)",
|
||||
],
|
||||
[
|
||||
"code" => "es-hn",
|
||||
"name" => "Spanish (Honduras)",
|
||||
],
|
||||
[
|
||||
"code" => "es-mx",
|
||||
"name" => "Spanish (Mexico)",
|
||||
],
|
||||
[
|
||||
"code" => "es-ni",
|
||||
"name" => "Spanish (Nicaragua)",
|
||||
],
|
||||
[
|
||||
"code" => "es-pa",
|
||||
"name" => "Spanish (Panama)",
|
||||
],
|
||||
[
|
||||
"code" => "es-pe",
|
||||
"name" => "Spanish (Peru)",
|
||||
],
|
||||
[
|
||||
"code" => "es-pr",
|
||||
"name" => "Spanish (Puerto Rico)",
|
||||
],
|
||||
[
|
||||
"code" => "es-py",
|
||||
"name" => "Spanish (Paraguay)",
|
||||
],
|
||||
[
|
||||
"code" => "es-sv",
|
||||
"name" => "Spanish (El Salvador)",
|
||||
],
|
||||
[
|
||||
"code" => "es-uy",
|
||||
"name" => "Spanish (Uruguay)",
|
||||
],
|
||||
[
|
||||
"code" => "es-ve",
|
||||
"name" => "Spanish (Venezuela)",
|
||||
],
|
||||
[
|
||||
"code" => "et",
|
||||
"name" => "Estonian",
|
||||
],
|
||||
[
|
||||
"code" => "eu",
|
||||
"name" => "Basque",
|
||||
],
|
||||
[
|
||||
"code" => "fa",
|
||||
"name" => "Farsi",
|
||||
],
|
||||
[
|
||||
"code" => "fi",
|
||||
"name" => "Finnish",
|
||||
],
|
||||
[
|
||||
"code" => "fo",
|
||||
"name" => "Faeroese",
|
||||
],
|
||||
[
|
||||
"code" => "fr",
|
||||
"name" => "French (Standard)",
|
||||
],
|
||||
[
|
||||
"code" => "fr-be",
|
||||
"name" => "French (Belgium)",
|
||||
],
|
||||
[
|
||||
"code" => "fr-ca",
|
||||
"name" => "French (Canada)",
|
||||
],
|
||||
[
|
||||
"code" => "fr-ch",
|
||||
"name" => "French (Switzerland)",
|
||||
],
|
||||
[
|
||||
"code" => "fr-lu",
|
||||
"name" => "French (Luxembourg)",
|
||||
],
|
||||
[
|
||||
"code" => "ga",
|
||||
"name" => "Irish",
|
||||
],
|
||||
[
|
||||
"code" => "gd",
|
||||
"name" => "Gaelic (Scotland)",
|
||||
],
|
||||
[
|
||||
"code" => "he",
|
||||
"name" => "Hebrew",
|
||||
],
|
||||
[
|
||||
"code" => "hi",
|
||||
"name" => "Hindi",
|
||||
],
|
||||
[
|
||||
"code" => "hr",
|
||||
"name" => "Croatian",
|
||||
],
|
||||
[
|
||||
"code" => "hu",
|
||||
"name" => "Hungarian",
|
||||
],
|
||||
[
|
||||
"code" => "id",
|
||||
"name" => "Indonesian",
|
||||
],
|
||||
[
|
||||
"code" => "is",
|
||||
"name" => "Icelandic",
|
||||
],
|
||||
[
|
||||
"code" => "it",
|
||||
"name" => "Italian (Standard)",
|
||||
],
|
||||
[
|
||||
"code" => "it-ch",
|
||||
"name" => "Italian (Switzerland)",
|
||||
],
|
||||
[
|
||||
"code" => "ja",
|
||||
"name" => "Japanese",
|
||||
],
|
||||
[
|
||||
"code" => "ji",
|
||||
"name" => "Yiddish",
|
||||
],
|
||||
[
|
||||
"code" => "ko",
|
||||
"name" => "Korean",
|
||||
],
|
||||
[
|
||||
"code" => "ku",
|
||||
"name" => "Kurdish",
|
||||
],
|
||||
[
|
||||
"code" => "lt",
|
||||
"name" => "Lithuanian",
|
||||
],
|
||||
[
|
||||
"code" => "lv",
|
||||
"name" => "Latvian",
|
||||
],
|
||||
[
|
||||
"code" => "mk",
|
||||
"name" => "Macedonian (FYROM)",
|
||||
],
|
||||
[
|
||||
"code" => "ml",
|
||||
"name" => "Malayalam",
|
||||
],
|
||||
[
|
||||
"code" => "ms",
|
||||
"name" => "Malaysian",
|
||||
],
|
||||
[
|
||||
"code" => "mt",
|
||||
"name" => "Maltese",
|
||||
],
|
||||
[
|
||||
"code" => "nb",
|
||||
"name" => "Norwegian (Bokmål)",
|
||||
],
|
||||
[
|
||||
"code" => "ne",
|
||||
"name" => "Nepali",
|
||||
],
|
||||
[
|
||||
"code" => "nl",
|
||||
"name" => "Dutch (Standard)",
|
||||
],
|
||||
[
|
||||
"code" => "nl-be",
|
||||
"name" => "Dutch (Belgium)",
|
||||
],
|
||||
[
|
||||
"code" => "nn",
|
||||
"name" => "Norwegian (Nynorsk)",
|
||||
],
|
||||
[
|
||||
"code" => "no",
|
||||
"name" => "Norwegian",
|
||||
],
|
||||
[
|
||||
"code" => "pa",
|
||||
"name" => "Punjabi",
|
||||
],
|
||||
[
|
||||
"code" => "pl",
|
||||
"name" => "Polish",
|
||||
],
|
||||
[
|
||||
"code" => "pt",
|
||||
"name" => "Portuguese (Portugal)",
|
||||
],
|
||||
[
|
||||
"code" => "pt-br",
|
||||
"name" => "Portuguese (Brazil)",
|
||||
],
|
||||
[
|
||||
"code" => "rm",
|
||||
"name" => "Rhaeto-Romanic",
|
||||
],
|
||||
[
|
||||
"code" => "ro",
|
||||
"name" => "Romanian",
|
||||
],
|
||||
[
|
||||
"code" => "ro-md",
|
||||
"name" => "Romanian (Republic of Moldova)",
|
||||
],
|
||||
[
|
||||
"code" => "ru",
|
||||
"name" => "Russian",
|
||||
],
|
||||
[
|
||||
"code" => "ru-md",
|
||||
"name" => "Russian (Republic of Moldova)",
|
||||
],
|
||||
[
|
||||
"code" => "sb",
|
||||
"name" => "Sorbian",
|
||||
],
|
||||
[
|
||||
"code" => "sk",
|
||||
"name" => "Slovak",
|
||||
],
|
||||
[
|
||||
"code" => "sl",
|
||||
"name" => "Slovenian",
|
||||
],
|
||||
[
|
||||
"code" => "sq",
|
||||
"name" => "Albanian",
|
||||
],
|
||||
[
|
||||
"code" => "sr",
|
||||
"name" => "Serbian",
|
||||
],
|
||||
[
|
||||
"code" => "sv",
|
||||
"name" => "Swedish",
|
||||
],
|
||||
[
|
||||
"code" => "sv-fi",
|
||||
"name" => "Swedish (Finland)",
|
||||
],
|
||||
[
|
||||
"code" => "th",
|
||||
"name" => "Thai",
|
||||
],
|
||||
[
|
||||
"code" => "tn",
|
||||
"name" => "Tswana",
|
||||
],
|
||||
[
|
||||
"code" => "tr",
|
||||
"name" => "Turkish",
|
||||
],
|
||||
[
|
||||
"code" => "ts",
|
||||
"name" => "Tsonga",
|
||||
],
|
||||
[
|
||||
"code" => "ua",
|
||||
"name" => "Ukrainian",
|
||||
],
|
||||
[
|
||||
"code" => "ur",
|
||||
"name" => "Urdu",
|
||||
],
|
||||
[
|
||||
"code" => "ve",
|
||||
"name" => "Venda",
|
||||
],
|
||||
[
|
||||
"code" => "vi",
|
||||
"name" => "Vietnamese",
|
||||
],
|
||||
[
|
||||
"code" => "xh",
|
||||
"name" => "Xhosa",
|
||||
],
|
||||
[
|
||||
"code" => "zh-cn",
|
||||
"name" => "Chinese (PRC)",
|
||||
],
|
||||
[
|
||||
"code" => "zh-hk",
|
||||
"name" => "Chinese (Hong Kong)",
|
||||
],
|
||||
[
|
||||
"code" => "zh-sg",
|
||||
"name" => "Chinese (Singapore)",
|
||||
],
|
||||
[
|
||||
"code" => "zh-tw",
|
||||
"name" => "Chinese (Taiwan)",
|
||||
],
|
||||
[
|
||||
"code" => "zu",
|
||||
"name" => "Zulu",
|
||||
],
|
||||
];
|
||||
|
|
|
|||
15
app/config/locale/templates.php
Normal file
15
app/config/locale/templates.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'email' => [
|
||||
'verification',
|
||||
'magicSession',
|
||||
'recovery',
|
||||
'invitation',
|
||||
],
|
||||
'sms' => [
|
||||
'verification',
|
||||
'login',
|
||||
'invitation'
|
||||
]
|
||||
];
|
||||
|
|
@ -2,164 +2,66 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>{{subject}}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: {{bg-body}};
|
||||
color: {{text-content}};
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@500;600&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
a { color:currentColor; }
|
||||
body {
|
||||
padding: 32px;
|
||||
color: #616B7C;
|
||||
font-size: 15px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0 !important;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
table,
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: {{bg-body}};
|
||||
width: 100%;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
h* {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
color: {{text-content}};
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #E8E9F0;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: {{bg-content}};
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 30px 30px 15px 30px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 620px) {
|
||||
.container {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="direction: {{direction}}">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table role="presentation" class="main">
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p>{{hello}}</p>
|
||||
<p>{{body}}</p>
|
||||
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
|
||||
<p></br>{{footer}}</p>
|
||||
<p>{{thanks}}
|
||||
</br>
|
||||
{{signature}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- <div style="text-align: center; line-height: 25px; margin: 15px 0; font-size: 12px; color: #40404c;">
|
||||
<a href="https://appwrite.io" style="text-decoration: none; color: #40404c;">Powered by <img src="https://appwrite.io/images/appwrite-footer-light.svg" height="15" style="margin: -3px 0" /></a>
|
||||
</div> -->
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="max-width:650px; word-wrap: break-wrod; overflow-wrap: break-word;
|
||||
word-break: break-all; margin:0 auto;">
|
||||
<table style="margin-top: 32px">
|
||||
<tr>
|
||||
<td>
|
||||
{{body}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
app/config/locale/templates/email-inner-base.tpl
Normal file
15
app/config/locale/templates/email-inner-base.tpl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<p>{{hello}}</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>{{body}}</p>
|
||||
|
||||
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
|
||||
|
||||
<p>{{footer}}</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>{{thanks}}</p>
|
||||
|
||||
<p>{{signature}}</p>
|
||||
1
app/config/locale/templates/sms-base.tpl
Normal file
1
app/config/locale/templates/sms-base.tpl
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{token}}
|
||||
|
|
@ -15,7 +15,7 @@ return [
|
|||
[
|
||||
'key' => 'web',
|
||||
'name' => 'Web',
|
||||
'version' => '11.0.0',
|
||||
'version' => '12.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-web',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -63,7 +63,7 @@ return [
|
|||
[
|
||||
'key' => 'flutter',
|
||||
'name' => 'Flutter',
|
||||
'version' => '9.0.0',
|
||||
'version' => '10.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-flutter',
|
||||
'package' => 'https://pub.dev/packages/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -81,7 +81,7 @@ return [
|
|||
[
|
||||
'key' => 'apple',
|
||||
'name' => 'Apple',
|
||||
'version' => '2.0.0',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'enabled' => true,
|
||||
|
|
@ -116,7 +116,7 @@ return [
|
|||
[
|
||||
'key' => 'android',
|
||||
'name' => 'Android',
|
||||
'version' => '2.0.0',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-android',
|
||||
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
|
||||
'enabled' => true,
|
||||
|
|
@ -185,7 +185,7 @@ return [
|
|||
[
|
||||
'key' => 'web',
|
||||
'name' => 'Console',
|
||||
'version' => '0.1.0',
|
||||
'version' => '0.3.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-console',
|
||||
'package' => '',
|
||||
'enabled' => true,
|
||||
|
|
@ -203,7 +203,7 @@ return [
|
|||
[
|
||||
'key' => 'cli',
|
||||
'name' => 'Command Line',
|
||||
'version' => '2.0.2',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
|
|
@ -231,7 +231,7 @@ return [
|
|||
[
|
||||
'key' => 'nodejs',
|
||||
'name' => 'Node.js',
|
||||
'version' => '9.0.0',
|
||||
'version' => '10.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-node',
|
||||
'package' => 'https://www.npmjs.com/package/node-appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -249,7 +249,7 @@ return [
|
|||
[
|
||||
'key' => 'deno',
|
||||
'name' => 'Deno',
|
||||
'version' => '7.0.0',
|
||||
'version' => '8.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-deno',
|
||||
'package' => 'https://deno.land/x/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -267,7 +267,7 @@ return [
|
|||
[
|
||||
'key' => 'php',
|
||||
'name' => 'PHP',
|
||||
'version' => '8.0.0',
|
||||
'version' => '9.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-php',
|
||||
'package' => 'https://packagist.org/packages/appwrite/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -285,7 +285,7 @@ return [
|
|||
[
|
||||
'key' => 'python',
|
||||
'name' => 'Python',
|
||||
'version' => '2.0.2',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-python',
|
||||
'package' => 'https://pypi.org/project/appwrite/',
|
||||
'enabled' => true,
|
||||
|
|
@ -303,7 +303,7 @@ return [
|
|||
[
|
||||
'key' => 'ruby',
|
||||
'name' => 'Ruby',
|
||||
'version' => '8.0.0',
|
||||
'version' => '9.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-ruby',
|
||||
'package' => 'https://rubygems.org/gems/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -375,7 +375,7 @@ return [
|
|||
[
|
||||
'key' => 'dart',
|
||||
'name' => 'Dart',
|
||||
'version' => '8.0.0',
|
||||
'version' => '9.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-dart',
|
||||
'package' => 'https://pub.dev/packages/dart_appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -393,7 +393,7 @@ return [
|
|||
[
|
||||
'key' => 'kotlin',
|
||||
'name' => 'Kotlin',
|
||||
'version' => '2.0.0',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
|
||||
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
|
||||
'enabled' => true,
|
||||
|
|
@ -415,7 +415,7 @@ return [
|
|||
[
|
||||
'key' => 'swift',
|
||||
'name' => 'Swift',
|
||||
'version' => '2.0.0',
|
||||
'version' => '3.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'enabled' => true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
return [ // Ordered by ABC.
|
||||
// Ordered by ABC.
|
||||
return [
|
||||
'amazon' => [
|
||||
'name' => 'Amazon',
|
||||
'developers' => 'https://developer.amazon.com/apps-and-games/services-and-apis',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use Appwrite\Auth\Auth;
|
||||
|
||||
$member = [
|
||||
'global',
|
||||
'public',
|
||||
'home',
|
||||
'console',
|
||||
|
|
@ -23,6 +24,7 @@ $member = [
|
|||
];
|
||||
|
||||
$admins = [
|
||||
'global',
|
||||
'graphql',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
|
|
@ -51,6 +53,10 @@ $admins = [
|
|||
'functions.write',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'rules.read',
|
||||
'rules.write',
|
||||
'migrations.read',
|
||||
'migrations.write',
|
||||
];
|
||||
|
||||
return [
|
||||
|
|
@ -72,22 +78,22 @@ return [
|
|||
],
|
||||
Auth::USER_ROLE_USERS => [
|
||||
'label' => 'Users',
|
||||
'scopes' => \array_merge($member, []),
|
||||
'scopes' => \array_merge($member),
|
||||
],
|
||||
Auth::USER_ROLE_ADMIN => [
|
||||
'label' => 'Admin',
|
||||
'scopes' => \array_merge($admins, []),
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
Auth::USER_ROLE_DEVELOPER => [
|
||||
'label' => 'Developer',
|
||||
'scopes' => \array_merge($admins, []),
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
Auth::USER_ROLE_OWNER => [
|
||||
'label' => 'Owner',
|
||||
'scopes' => \array_merge($member, $admins, []),
|
||||
'scopes' => \array_merge($member, $admins),
|
||||
],
|
||||
Auth::USER_ROLE_APPS => [
|
||||
'label' => 'Applications',
|
||||
'scopes' => ['health.read', 'graphql'],
|
||||
'scopes' => ['global', 'health.read', 'graphql'],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
use Utopia\App;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
|
||||
$runtimes = new Runtimes('v2');
|
||||
$runtimes = new Runtimes('v3');
|
||||
|
||||
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
|
||||
|
||||
|
|
|
|||
|
|
@ -76,4 +76,16 @@ return [ // List of publicly visible scopes
|
|||
'health.read' => [
|
||||
'description' => 'Access to read your project\'s health status',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read your project\'s proxy rules',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s proxy rules',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read your project\'s migrations',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s migrations.',
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -108,6 +108,19 @@ return [
|
|||
'optional' => false,
|
||||
'icon' => '',
|
||||
],
|
||||
'project' => [
|
||||
'key' => 'project',
|
||||
'name' => 'Project',
|
||||
'subtitle' => 'The Project service allows you to manage all the projects in your Appwrite server.',
|
||||
'description' => '',
|
||||
'controller' => 'api/project.php',
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => false,
|
||||
'icon' => '',
|
||||
],
|
||||
'storage' => [
|
||||
'key' => 'storage',
|
||||
'name' => 'Storage',
|
||||
|
|
@ -147,6 +160,19 @@ return [
|
|||
'optional' => true,
|
||||
'icon' => '/images/services/users.png',
|
||||
],
|
||||
'vcs' => [
|
||||
'key' => 'vcs',
|
||||
'name' => 'VCS',
|
||||
'subtitle' => 'The VCS service allows you to interact with providers like GitHub, GitLab etc.',
|
||||
'description' => '',
|
||||
'controller' => 'api/vcs.php',
|
||||
'sdk' => false,
|
||||
'docs' => false,
|
||||
'docsUrl' => '',
|
||||
'tests' => false,
|
||||
'optional' => true,
|
||||
'icon' => '',
|
||||
],
|
||||
'functions' => [
|
||||
'key' => 'functions',
|
||||
'name' => 'Functions',
|
||||
|
|
@ -160,6 +186,19 @@ return [
|
|||
'optional' => true,
|
||||
'icon' => '/images/services/functions.png',
|
||||
],
|
||||
'proxy' => [
|
||||
'key' => 'proxy',
|
||||
'name' => 'Proxy',
|
||||
'subtitle' => 'The Proxy Service allows you to configure actions for your domains beyond DNS configuration.',
|
||||
'description' => '/docs/services/proxy.md',
|
||||
'controller' => 'api/proxy.php',
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/proxy',
|
||||
'tests' => false,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/proxy.png',
|
||||
],
|
||||
'mock' => [
|
||||
'key' => 'mock',
|
||||
'name' => 'Mock',
|
||||
|
|
@ -199,4 +238,17 @@ return [
|
|||
'optional' => false,
|
||||
'icon' => '',
|
||||
],
|
||||
'migrations' => [
|
||||
'key' => 'migrations',
|
||||
'name' => 'Migrations',
|
||||
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite project.',
|
||||
'description' => '/docs/services/migrations.md',
|
||||
'controller' => 'api/migrations.php',
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
'tests' => true,
|
||||
'optional' => true,
|
||||
'icon' => '/images/services/migrations.png',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
1
app/config/specs/open-api3-1.4.x-client.json
Normal file
1
app/config/specs/open-api3-1.4.x-client.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/open-api3-1.4.x-console.json
Normal file
1
app/config/specs/open-api3-1.4.x-console.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/open-api3-1.4.x-server.json
Normal file
1
app/config/specs/open-api3-1.4.x-server.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/config/specs/swagger2-1.4.x-client.json
Normal file
1
app/config/specs/swagger2-1.4.x-client.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/swagger2-1.4.x-console.json
Normal file
1
app/config/specs/swagger2-1.4.x-console.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/swagger2-1.4.x-server.json
Normal file
1
app/config/specs/swagger2-1.4.x-server.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -31,6 +31,8 @@ return [
|
|||
'audio/ogg', // Ogg Vorbis RFC 5334
|
||||
'audio/vorbis', // Vorbis RFC 5215
|
||||
'audio/vnd.wav', // wav RFC 2361
|
||||
'audio/aac', //AAC audio
|
||||
'audio/x-hx-aac-adts', // AAC audio
|
||||
|
||||
// Microsoft Word
|
||||
'application/msword',
|
||||
|
|
|
|||
24
app/config/usage.php
Normal file
24
app/config/usage.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'24h' => [
|
||||
'period' => '1h',
|
||||
'limit' => 24,
|
||||
'factor' => 3600,
|
||||
],
|
||||
'7d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 7,
|
||||
'factor' => 86400,
|
||||
],
|
||||
'30d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 30,
|
||||
'factor' => 86400,
|
||||
],
|
||||
'90d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 90,
|
||||
'factor' => 86400,
|
||||
],
|
||||
];
|
||||
|
|
@ -61,6 +61,15 @@ return [
|
|||
'question' => 'Enter your Appwrite hostname',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_DOMAIN_FUNCTIONS',
|
||||
'description' => 'A domain to use for function preview URLs. Setting to empty turns off function preview URLs.',
|
||||
'introduction' => '',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_DOMAIN_TARGET',
|
||||
'description' => 'A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.',
|
||||
|
|
@ -143,7 +152,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_USAGE_STATS',
|
||||
'description' => 'This variable allows you to disable the collection and displaying of usage stats. This value is set to \'enabled\' by default, to disable the usage stats set the value to \'disabled\'. When disabled, it\'s recommended to turn off the Worker Usage, Influxdb and Telegraf containers for better resource usage.',
|
||||
'description' => 'This variable allows you to disable the collection and displaying of usage stats. This value is set to \'enabled\' by default, to disable the usage stats set the value to \'disabled\'. When disabled, it\'s recommended to turn off the Worker Usage container to reduce resource usage.',
|
||||
'introduction' => '0.7.0',
|
||||
'default' => 'enabled',
|
||||
'required' => false,
|
||||
|
|
@ -152,7 +161,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_LOGGING_PROVIDER',
|
||||
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appSignal\', \'logOwl\'',
|
||||
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, set the value to one of \'sentry\', \'raygun\', \'appSignal\', \'logOwl\' to enable the logger.',
|
||||
'introduction' => '0.12.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
@ -203,7 +212,7 @@ return [
|
|||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
|
@ -413,7 +422,7 @@ return [
|
|||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_SMS_PROVIDER',
|
||||
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
|
||||
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
|
||||
'introduction' => '0.15.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
@ -683,7 +692,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_CONTAINERS',
|
||||
'description' => 'The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10.',
|
||||
'description' => 'Deprecated since 1.2.0. Runtimes now timeout by inactivity using \'_APP_FUNCTIONS_INACTIVE_THRESHOLD\'.',
|
||||
'introduction' => '0.7.0',
|
||||
'default' => '10',
|
||||
'required' => false,
|
||||
|
|
@ -710,7 +719,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_MEMORY_SWAP',
|
||||
'description' => 'The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it\'s empty, swap memory limit will be disabled.',
|
||||
'description' => 'Deprecated since 1.2.0. High use of swap memory is not recommended to preserve harddrive health.',
|
||||
'introduction' => '0.7.0',
|
||||
'default' => '0',
|
||||
'required' => false,
|
||||
|
|
@ -773,7 +782,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => 'DOCKERHUB_PULL_USERNAME',
|
||||
'description' => 'The username for hub.docker.com. This variable is used to pull images from hub.docker.com.',
|
||||
'description' => 'Deprecated with 1.2.0, use \'_APP_DOCKER_HUB_USERNAME\' instead!',
|
||||
'introduction' => '0.10.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
@ -782,7 +791,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => 'DOCKERHUB_PULL_PASSWORD',
|
||||
'description' => 'The password for hub.docker.com. This variable is used to pull images from hub.docker.com.',
|
||||
'description' => 'Deprecated with 1.2.0, use \'_APP_DOCKER_HUB_PASSWORD\' instead!',
|
||||
'introduction' => '0.10.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
@ -791,7 +800,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => 'DOCKERHUB_PULL_EMAIL',
|
||||
'description' => 'The email for hub.docker.com. This variable is used to pull images from hub.docker.com.',
|
||||
'description' => 'Deprecated since 1.2.0. Email is no longer needed.',
|
||||
'introduction' => '0.10.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
@ -800,13 +809,109 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => 'OPEN_RUNTIMES_NETWORK',
|
||||
'description' => 'The docker network used for communication between the executor and runtimes. Change this if you have altered the default network names.',
|
||||
'description' => 'Deprecated with 1.2.0, use \'_APP_FUNCTIONS_RUNTIMES_NETWORK\' instead!',
|
||||
'introduction' => '0.13.0',
|
||||
'default' => 'appwrite_runtimes',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_RUNTIMES_NETWORK',
|
||||
'description' => 'The docker network used for communication between the executor and runtimes.',
|
||||
'introduction' => '1.2.0',
|
||||
'default' => 'runtimes',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_DOCKER_HUB_USERNAME',
|
||||
'description' => 'The username for hub.docker.com. This variable is used to pull images from hub.docker.com.',
|
||||
'introduction' => '1.2.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_DOCKER_HUB_PASSWORD',
|
||||
'description' => 'The password for hub.docker.com. This variable is used to pull images from hub.docker.com.',
|
||||
'introduction' => '1.2.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_FUNCTIONS_MAINTENANCE_INTERVAL',
|
||||
'description' => 'Interval how often executor checks for inactive runimes. The default value is 60 seconds.',
|
||||
'introduction' => '1.2.0',
|
||||
'default' => '60',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'VCS (Version Control System)',
|
||||
'description' => '',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_APP_NAME',
|
||||
'description' => 'Name of your GitHub app. This value should be set to your GitHub application\'s URL.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_PRIVATE_KEY',
|
||||
'description' => 'GitHub app RSA private key. You can generate private keys from GitHub application settings.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_APP_ID',
|
||||
'description' => 'GitHub application ID. You can find it in your GitHub application details.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_CLIENT_ID',
|
||||
'description' => 'GitHub client ID. You can find it in your GitHub application details.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_CLIENT_SECRET',
|
||||
'description' => 'GitHub client secret. You can generate secrets in your GitHub application settings.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_VCS_GITHUB_WEBHOOK_SECRET',
|
||||
'description' => 'GitHub webhook secret. You can configure it in your GitHub application settings under webhook section.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
|
@ -867,6 +972,15 @@ return [
|
|||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_RETENTION_SCHEDULES',
|
||||
'description' => 'Schedules deletion interval ( in seconds ) ',
|
||||
'introduction' => 'TBD',
|
||||
'default' => '86400',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
]
|
||||
],
|
||||
],
|
||||
[
|
||||
|
|
@ -902,4 +1016,43 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'Migrations',
|
||||
'description' => '',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID',
|
||||
'description' => 'Google OAuth client ID. You can find it in your GCP application settings.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET',
|
||||
'description' => 'Google OAuth client secret. You can generate secrets in your GCP application settings.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'category' => 'Assistant',
|
||||
'description' => '',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_ASSISTANT_OPENAI_API_KEY',
|
||||
'description' => 'OpenAI API key. You can find it in your OpenAI application settings.',
|
||||
'introduction' => '1.4.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 9174d8f8cb584744dd7a53f69d324f490ee82ee3
|
||||
Subproject commit bfd14ef923e57938639e95e5ab32e68d49e24407
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Validator\URL;
|
||||
use Appwrite\URL\URL as URLParse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Image\Image;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
|
||||
$avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) {
|
||||
|
||||
|
|
@ -35,7 +43,7 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
|
|||
}
|
||||
|
||||
$output = 'png';
|
||||
$path = $set[$code];
|
||||
$path = $set[$code]['path'];
|
||||
$type = 'png';
|
||||
|
||||
if (!\is_readable($path)) {
|
||||
|
|
@ -49,11 +57,139 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->file($data)
|
||||
;
|
||||
->file($data);
|
||||
unset($image);
|
||||
};
|
||||
|
||||
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) {
|
||||
try {
|
||||
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
$gitHubSession = null;
|
||||
foreach ($sessions as $session) {
|
||||
if ($session->getAttribute('provider', '') === 'github') {
|
||||
$gitHubSession = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($gitHubSession)) {
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'GitHub session not found.');
|
||||
}
|
||||
|
||||
$provider = $gitHubSession->getAttribute('provider', '');
|
||||
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
|
||||
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
|
||||
|
||||
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
|
||||
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
|
||||
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
|
||||
|
||||
if (!\class_exists($className)) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
try {
|
||||
$oauth2->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $oauth2->getAccessToken('');
|
||||
$refreshToken = $oauth2->getRefreshToken('');
|
||||
|
||||
$verificationId = $oauth2->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new \Exception("Locked tokens."); // Race codition, handeled in catch
|
||||
}
|
||||
|
||||
$gitHubSession
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('sessions', $gitHubSession->getId(), $gitHubSession));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
} catch (Throwable $err) {
|
||||
$index = 0;
|
||||
do {
|
||||
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
|
||||
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
$gitHubSession = new Document();
|
||||
foreach ($sessions as $session) {
|
||||
if ($session->getAttribute('provider', '') === 'github') {
|
||||
$gitHubSession = $session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$accessToken = $gitHubSession->getAttribute('providerAccessToken');
|
||||
|
||||
if ($accessToken !== $previousAccessToken) {
|
||||
break;
|
||||
}
|
||||
|
||||
$index++;
|
||||
\usleep(500000);
|
||||
} while ($index < 10);
|
||||
}
|
||||
}
|
||||
|
||||
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
||||
$githubUser = $oauth2->getUserSlug($accessToken);
|
||||
$githubId = $oauth2->getUserID($accessToken);
|
||||
|
||||
return [
|
||||
'name' => $githubUser,
|
||||
'id' => $githubId
|
||||
];
|
||||
} catch (Exception $error) {
|
||||
if ($logger) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace('console');
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
|
||||
$log->setAction('avatarsGetGitHub');
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('GitHub error log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
App::get('/v1/avatars/credit-cards/:code')
|
||||
->desc('Get Credit Card Icon')
|
||||
->groups(['api', 'avatars'])
|
||||
|
|
@ -141,7 +277,13 @@ App::get('/v1/avatars/image')
|
|||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$fetch = @\file_get_contents($url, false);
|
||||
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$fetch = @\file_get_contents($url);
|
||||
|
||||
if (!$fetch) {
|
||||
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND);
|
||||
|
|
@ -160,8 +302,7 @@ App::get('/v1/avatars/image')
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->file($data)
|
||||
;
|
||||
->file($data);
|
||||
unset($image);
|
||||
});
|
||||
|
||||
|
|
@ -192,6 +333,12 @@ App::get('/v1/avatars/favicon')
|
|||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($url, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
$curl = \curl_init();
|
||||
|
||||
\curl_setopt_array($curl, [
|
||||
|
|
@ -265,6 +412,12 @@ App::get('/v1/avatars/favicon')
|
|||
$outputExt = 'ico';
|
||||
}
|
||||
|
||||
$domain = new Domain(\parse_url($outputHref, PHP_URL_HOST));
|
||||
|
||||
if (!$domain->isKnown()) {
|
||||
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
|
||||
}
|
||||
|
||||
if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files
|
||||
$data = @\file_get_contents($outputHref, false);
|
||||
|
||||
|
|
@ -274,8 +427,7 @@ App::get('/v1/avatars/favicon')
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/x-icon')
|
||||
->file($data)
|
||||
;
|
||||
->file($data);
|
||||
}
|
||||
|
||||
$fetch = @\file_get_contents($outputHref, false);
|
||||
|
|
@ -292,8 +444,7 @@ App::get('/v1/avatars/favicon')
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->file($data)
|
||||
;
|
||||
->file($data);
|
||||
unset($image);
|
||||
});
|
||||
|
||||
|
|
@ -334,8 +485,7 @@ App::get('/v1/avatars/qr')
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->send($image->output('png', 9))
|
||||
;
|
||||
->send($image->output('png', 9));
|
||||
});
|
||||
|
||||
App::get('/v1/avatars/initials')
|
||||
|
|
@ -414,11 +564,683 @@ App::get('/v1/avatars/initials')
|
|||
$image->setImageFormat("png");
|
||||
$image->compositeImage($punch, Imagick::COMPOSITE_COPYOPACITY, 0, 0);
|
||||
|
||||
//$image->setImageCompressionQuality(9 - round(($quality / 100) * 9));
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->file($image->getImageBlob());
|
||||
});
|
||||
|
||||
App::get('/v1/cards/cloud')
|
||||
->desc('Get Front Of Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud')
|
||||
->label('cache.resource', 'card/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
|
||||
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
|
||||
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$name = $user->getAttribute('name', 'Anonymous');
|
||||
$email = $user->getAttribute('email', '');
|
||||
$createdAt = new \DateTime($user->getCreatedAt());
|
||||
|
||||
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
|
||||
$githubName = $gitHub['name'] ?? '';
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
|
||||
|
||||
if ($isHero) {
|
||||
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
|
||||
} elseif ($isEmployee) {
|
||||
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$isPlatinum = $user->getInternalId() % 100 === 0;
|
||||
} else {
|
||||
$name = $mock === 'normal-long' ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
|
||||
$createdAt = new \DateTime('now');
|
||||
$githubName = $mock === 'normal-no-github' ? '' : ($mock === 'normal-long' ? 'sir-first-walterobrian-junior' : 'walterobrian');
|
||||
$isHero = $mock === 'hero';
|
||||
$isContributor = $mock === 'contributor';
|
||||
$isEmployee = \str_starts_with($mock, 'employee');
|
||||
$employeeNumber = match ($mock) {
|
||||
'employee' => '1',
|
||||
'employee-2digit' => '18',
|
||||
default => ''
|
||||
};
|
||||
|
||||
$isPlatinum = $mock === 'platinum';
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$isContributor = false;
|
||||
$isHero = false;
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$isContributor = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$isHero = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
|
||||
|
||||
$imagePath = $isGolden ? 'front-golden.png' : ($isPlatinum ? 'front-platinum.png' : 'front.png');
|
||||
|
||||
$baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath);
|
||||
|
||||
if ($isEmployee) {
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/employee.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 35);
|
||||
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf');
|
||||
$text->setFillColor(new \ImagickPixel('#FFFADF'));
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 2 ? 54 : 48);
|
||||
$text->setFontWeight(700);
|
||||
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
|
||||
|
||||
$hashtag = new \ImagickDraw();
|
||||
$hashtag->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf');
|
||||
$hashtag->setFillColor(new \ImagickPixel('#FFFADF'));
|
||||
$hashtag->setFontSize(28);
|
||||
$hashtag->setFontWeight(700);
|
||||
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
|
||||
|
||||
$startX = 898;
|
||||
$totalWidth = $metricsHashtag['textWidth'] + 12 + $metricsText['textWidth'];
|
||||
|
||||
$hashtagX = ($metricsHashtag['textWidth'] / 2);
|
||||
$textX = $hashtagX + 12 + ($metricsText['textWidth'] / 2);
|
||||
|
||||
$hashtagX -= $totalWidth / 2;
|
||||
$textX -= $totalWidth / 2;
|
||||
|
||||
$hashtagX += $startX;
|
||||
$textX += $startX;
|
||||
|
||||
$baseImage->annotateImage($hashtag, $hashtagX, 150, 0, '#');
|
||||
$baseImage->annotateImage($text, $textX, 150, 0, $employeeNumber);
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/contributor.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/hero.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 793, 34);
|
||||
}
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
|
||||
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
|
||||
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
|
||||
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Poppins-Bold.ttf');
|
||||
$text->setFillColor(new \ImagickPixel('#FFFFFF'));
|
||||
|
||||
if (\strlen($name) > 32) {
|
||||
$name = \substr($name, 0, 32);
|
||||
}
|
||||
|
||||
if (\strlen($name) <= 23) {
|
||||
$text->setFontSize(80);
|
||||
$scalingDown = false;
|
||||
} else {
|
||||
$text->setFontSize(54);
|
||||
$scalingDown = true;
|
||||
}
|
||||
$text->setFontWeight(700);
|
||||
$baseImage->annotateImage($text, 512, 477, 0, $name);
|
||||
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Inter-SemiBold.ttf');
|
||||
$text->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
|
||||
$text->setFontSize(27);
|
||||
$text->setFontWeight(600);
|
||||
$text->setTextKerning(1.08);
|
||||
$baseImage->annotateImage($text, 512, 541, 0, \strtoupper($memberSince));
|
||||
|
||||
if (!empty($githubName)) {
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf');
|
||||
$text->setFillColor(new \ImagickPixel('#FFFFFF'));
|
||||
$text->setFontSize($scalingDown ? 28 : 32);
|
||||
$text->setFontWeight(400);
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$baseImage->annotateImage($text, 512 + 20 + 4, 373 + ($scalingDown ? 2 : 0), 0, $githubName);
|
||||
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$precisionFix = 5;
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2) - 20 - 4, 373 - ($metrics['textHeight'] - $precisionFix));
|
||||
}
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->file($image->getImageBlob())
|
||||
;
|
||||
->file($baseImage->getImageBlob());
|
||||
});
|
||||
|
||||
App::get('/v1/cards/cloud-back')
|
||||
->desc('Get Back Of Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud-back')
|
||||
->label('cache.resource', 'card-back/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['golden', 'normal', 'platinum']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 512), 'Resize image width, Pass an integer between 0 to 512.', true)
|
||||
->param('height', 0, new Range(0, 320), 'Resize image height, Pass an integer between 0 to 320.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
|
||||
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$userId = $user->getId();
|
||||
$email = $user->getAttribute('email', '');
|
||||
|
||||
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $user->getInternalId() % 100 === 0;
|
||||
} else {
|
||||
$userId = '63e0bcf3c3eb803ba530';
|
||||
|
||||
$isGolden = $mock === 'golden';
|
||||
$isPlatinum = $mock === 'platinum';
|
||||
}
|
||||
|
||||
$userId = 'UID ' . $userId;
|
||||
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
|
||||
$imagePath = $isGolden ? 'back-golden.png' : ($isPlatinum ? 'back-platinum.png' : 'back.png');
|
||||
|
||||
$baseImage = new \Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $imagePath);
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $userId = \iconv("utf-8", "ascii//TRANSLIT", $userId);
|
||||
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/SourceCodePro-Regular.ttf');
|
||||
$text->setFillColor(new \ImagickPixel($isGolden ? '#664A1E' : ($isPlatinum ? '#555555' : '#E8E9F0')));
|
||||
$text->setFontSize(28);
|
||||
$text->setFontWeight(400);
|
||||
$baseImage->annotateImage($text, 512, 596, 0, $userId);
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->file($baseImage->getImageBlob());
|
||||
});
|
||||
|
||||
App::get('/v1/cards/cloud-og')
|
||||
->desc('Get OG Image From Cloud Card')
|
||||
->groups(['api', 'avatars'])
|
||||
->label('scope', 'avatars.read')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'cards/cloud-og')
|
||||
->label('cache.resource', 'card-og/{request.userId}')
|
||||
->label('docs', false)
|
||||
->label('origin', '*')
|
||||
->param('userId', '', new UID(), 'User ID.', true)
|
||||
->param('mock', '', new WhiteList(['employee', 'employee-2digit', 'hero', 'contributor', 'normal', 'platinum', 'normal-no-github', 'normal-long', 'normal-long-right', 'normal-long-middle', 'normal-bg2', 'normal-bg3', 'normal-right', 'normal-middle', 'platinum-right', 'platinum-middle', 'hero-middle', 'hero-right', 'contributor-right', 'employee-right', 'contributor-middle', 'employee-middle', 'employee-2digit-middle', 'employee-2digit-right']), 'Mocking behaviour.', true)
|
||||
->param('width', 0, new Range(0, 1024), 'Resize image card width, Pass an integer between 0 to 1024.', true)
|
||||
->param('height', 0, new Range(0, 1024), 'Resize image card height, Pass an integer between 0 to 1024.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('response')
|
||||
->inject('heroes')
|
||||
->inject('contributors')
|
||||
->inject('employees')
|
||||
->inject('logger')
|
||||
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
|
||||
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty() && empty($mock)) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$mock) {
|
||||
$internalId = $user->getInternalId();
|
||||
$bgVariation = $internalId % 3 === 0 ? '1' : ($internalId % 3 === 1 ? '2' : '3');
|
||||
$cardVariation = $internalId % 3 === 0 ? '1' : ($internalId % 3 === 1 ? '2' : '3');
|
||||
|
||||
$name = $user->getAttribute('name', 'Anonymous');
|
||||
$email = $user->getAttribute('email', '');
|
||||
$createdAt = new \DateTime($user->getCreatedAt());
|
||||
|
||||
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
|
||||
$githubName = $gitHub['name'] ?? '';
|
||||
$githubId = $gitHub['id'] ?? '';
|
||||
|
||||
$isHero = \array_key_exists($email, $heroes);
|
||||
$isContributor = \in_array($githubId, $contributors);
|
||||
$isEmployee = \array_key_exists($email, $employees);
|
||||
$employeeNumber = $isEmployee ? $employees[$email]['spot'] : '';
|
||||
|
||||
if ($isHero) {
|
||||
$createdAt = new \DateTime($heroes[$email]['memberSince'] ?? '');
|
||||
} elseif ($isEmployee) {
|
||||
$createdAt = new \DateTime($employees[$email]['memberSince'] ?? '');
|
||||
}
|
||||
|
||||
if (!$isEmployee && !empty($githubName)) {
|
||||
$employeeGitHub = \array_search(\strtolower($githubName), \array_map(fn ($employee) => \strtolower($employee['gitHub']) ?? '', $employees));
|
||||
if (!empty($employeeGitHub)) {
|
||||
$isEmployee = true;
|
||||
$employeeNumber = $isEmployee ? $employees[$employeeGitHub]['spot'] : '';
|
||||
$createdAt = new \DateTime($employees[$employeeGitHub]['memberSince'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$isPlatinum = $user->getInternalId() % 100 === 0;
|
||||
} else {
|
||||
$bgVariation = \str_ends_with($mock, '-bg2') ? '2' : (\str_ends_with($mock, '-bg3') ? '3' : '1');
|
||||
$cardVariation = \str_ends_with($mock, '-right') ? '2' : (\str_ends_with($mock, '-middle') ? '3' : '1');
|
||||
$name = \str_starts_with($mock, 'normal-long') ? 'Sir First Walter O\'Brian Junior' : 'Walter O\'Brian';
|
||||
$createdAt = new \DateTime('now');
|
||||
$githubName = $mock === 'normal-no-github' ? '' : (\str_starts_with($mock, 'normal-long') ? 'sir-first-walterobrian-junior' : 'walterobrian');
|
||||
$isHero = \str_starts_with($mock, 'hero');
|
||||
$isContributor = \str_starts_with($mock, 'contributor');
|
||||
$isEmployee = \str_starts_with($mock, 'employee');
|
||||
$employeeNumber = match ($mock) {
|
||||
'employee' => '1',
|
||||
'employee-right' => '1',
|
||||
'employee-middle' => '1',
|
||||
'employee-2digit' => '18',
|
||||
'employee-2digit-right' => '18',
|
||||
'employee-2digit-middle' => '18',
|
||||
default => ''
|
||||
};
|
||||
|
||||
$isPlatinum = \str_starts_with($mock, 'platinum');
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$isContributor = false;
|
||||
$isHero = false;
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$isContributor = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$isHero = false;
|
||||
$isEmployee = false;
|
||||
}
|
||||
|
||||
$isGolden = $isEmployee || $isHero || $isContributor;
|
||||
$isPlatinum = $isGolden ? false : $isPlatinum;
|
||||
$memberSince = \strtoupper('Member since ' . $createdAt->format('M') . ' ' . $createdAt->format('d') . ', ' . $createdAt->format('o'));
|
||||
|
||||
$baseImage = new \Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-background{$bgVariation}.png");
|
||||
|
||||
$cardType = $isGolden ? '-golden' : ($isPlatinum ? '-platinum' : '');
|
||||
|
||||
$image = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-card{$cardType}{$cardVariation}.png");
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 1008 / 2 - $image->getImageWidth() / 2, 1008 / 2 - $image->getImageHeight() / 2);
|
||||
|
||||
$imageLogo = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/og-background-logo.png');
|
||||
$imageShadow = new Imagick(__DIR__ . "/../../../public/images/cards/cloud/og-shadow{$cardType}.png");
|
||||
if ($cardVariation === '1') {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -450, 700);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -20, 710);
|
||||
} else {
|
||||
$baseImage->compositeImage($imageLogo, Imagick::COMPOSITE_OVER, 1008 - $imageLogo->getImageWidth() - 32, 1008 - $imageLogo->getImageHeight() - 32);
|
||||
$baseImage->compositeImage($imageShadow, Imagick::COMPOSITE_OVER, -135, 710);
|
||||
}
|
||||
|
||||
if ($isEmployee) {
|
||||
$file = $cardVariation === '3' ? 'employee-skew.png' : 'employee.png';
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
$hashtag = new \ImagickDraw();
|
||||
$hashtag->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$hashtag->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf');
|
||||
$hashtag->setFillColor(new \ImagickPixel('#FFFADF'));
|
||||
$hashtag->setFontSize(20);
|
||||
$hashtag->setFontWeight(700);
|
||||
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Inter-Bold.ttf');
|
||||
$text->setFillColor(new \ImagickPixel('#FFFADF'));
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 36 : 28);
|
||||
$text->setFontWeight(700);
|
||||
|
||||
if ($cardVariation === '3') {
|
||||
$hashtag->setFontSize(16);
|
||||
$text->setFontSize(\strlen($employeeNumber) <= 1 ? 30 : 26);
|
||||
|
||||
$hashtag->skewY(20);
|
||||
$hashtag->skewX(20);
|
||||
$text->skewY(20);
|
||||
$text->skewX(20);
|
||||
}
|
||||
|
||||
$metricsHashtag = $baseImage->queryFontMetrics($hashtag, '#');
|
||||
$metricsText = $baseImage->queryFontMetrics($text, $employeeNumber);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metricsHashtag['textWidth'] + 6 + $metricsText['textWidth'];
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
|
||||
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
|
||||
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
|
||||
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), -22);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 660, 245);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 655, 247);
|
||||
}
|
||||
} elseif ($cardVariation === '2') {
|
||||
$group->newImage($groupWidth, $metricsText['textHeight'], '#00000000');
|
||||
$group->annotateImage($hashtag, 0, $metricsText['textHeight'], 0, '#');
|
||||
$group->annotateImage($text, $metricsHashtag['textWidth'] + 6, $metricsText['textHeight'], 0, $employeeNumber);
|
||||
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), 32);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 775, 465);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 767, 470);
|
||||
}
|
||||
} else {
|
||||
$group->newImage(300, 300, '#00000000');
|
||||
|
||||
$hashtag->annotation(0, $metricsText['textHeight'], '#');
|
||||
$text->annotation($metricsHashtag['textWidth'] + 2, $metricsText['textHeight'], $employeeNumber);
|
||||
|
||||
$group->drawImage($hashtag);
|
||||
$group->drawImage($text);
|
||||
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
|
||||
if (\strlen($employeeNumber) <= 1) {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 670, 317);
|
||||
} else {
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, 663, 322);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isContributor) {
|
||||
$file = $cardVariation === '3' ? 'contributor-skew.png' : 'contributor.png';
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
} else {
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isHero) {
|
||||
$file = $cardVariation === '3' ? 'hero-skew.png' : 'hero.png';
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/' . $file);
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), -20);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 612, 203);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$image->resizeImage(120, 120, Imagick::FILTER_LANCZOS, 1);
|
||||
$image->rotateImage(new ImagickPixel('#00000000'), 30);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 715, 425);
|
||||
} else {
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 640, 293);
|
||||
}
|
||||
}
|
||||
|
||||
setlocale(LC_ALL, "en_US.utf8");
|
||||
// $name = \iconv("utf-8", "ascii//TRANSLIT", $name);
|
||||
// $memberSince = \iconv("utf-8", "ascii//TRANSLIT", $memberSince);
|
||||
// $githubName = \iconv("utf-8", "ascii//TRANSLIT", $githubName);
|
||||
|
||||
$textName = new \ImagickDraw();
|
||||
$textName->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$textName->setFont(__DIR__ . '/../../../public/fonts/Poppins-Bold.ttf');
|
||||
$textName->setFillColor(new \ImagickPixel('#FFFFFF'));
|
||||
|
||||
if (\strlen($name) > 32) {
|
||||
$name = \substr($name, 0, 32);
|
||||
}
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(54);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(36);
|
||||
}
|
||||
} elseif ($cardVariation === '2') {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(50);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(34);
|
||||
}
|
||||
} else {
|
||||
if (\strlen($name) <= 23) {
|
||||
$scalingDown = false;
|
||||
$textName->setFontSize(44);
|
||||
} else {
|
||||
$scalingDown = true;
|
||||
$textName->setFontSize(32);
|
||||
}
|
||||
}
|
||||
|
||||
$textName->setFontWeight(700);
|
||||
|
||||
$textMember = new \ImagickDraw();
|
||||
$textMember->setTextAlignment(Imagick::ALIGN_CENTER);
|
||||
$textMember->setFont(__DIR__ . '/../../../public/fonts/Inter-Medium.ttf');
|
||||
$textMember->setFillColor(new \ImagickPixel($isGolden || $isPlatinum ? '#FFFFFF' : '#FFB9CC'));
|
||||
$textMember->setFontWeight(500);
|
||||
$textMember->setTextKerning(1.12);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$textMember->setFontSize(21);
|
||||
|
||||
$baseImage->annotateImage($textName, 550, 600, -22, $name);
|
||||
$baseImage->annotateImage($textMember, 585, 635, -22, $memberSince);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$textMember->setFontSize(20);
|
||||
|
||||
$baseImage->annotateImage($textName, 435, 590, 31.37, $name);
|
||||
$baseImage->annotateImage($textMember, 412, 628, 31.37, $memberSince);
|
||||
} else {
|
||||
$textMember->setFontSize(16);
|
||||
|
||||
$textName->skewY(20);
|
||||
$textName->skewX(20);
|
||||
$textName->annotation(320, 700, $name);
|
||||
|
||||
$textMember->skewY(20);
|
||||
$textMember->skewX(20);
|
||||
$textMember->annotation(330, 735, $memberSince);
|
||||
|
||||
$baseImage->drawImage($textName);
|
||||
$baseImage->drawImage($textMember);
|
||||
}
|
||||
|
||||
if (!empty($githubName)) {
|
||||
$text = new \ImagickDraw();
|
||||
$text->setTextAlignment(Imagick::ALIGN_LEFT);
|
||||
$text->setFont(__DIR__ . '/../../../public/fonts/Inter-Regular.ttf');
|
||||
$text->setFillColor(new \ImagickPixel('#FFFFFF'));
|
||||
$text->setFontSize($scalingDown ? 16 : 20);
|
||||
$text->setFontWeight(400);
|
||||
|
||||
if ($cardVariation === '1') {
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metrics['textWidth'] + 32 + 4;
|
||||
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
|
||||
$precisionFix = -1;
|
||||
|
||||
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
|
||||
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), -22);
|
||||
$x = 510 - $group->getImageWidth() / 2;
|
||||
$y = 530 - $group->getImageHeight() / 2;
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
|
||||
} elseif ($cardVariation === '2') {
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$group = new Imagick();
|
||||
$groupWidth = $metrics['textWidth'] + 32 + 4;
|
||||
$group->newImage($groupWidth, $metrics['textHeight'] + 10, '#00000000');
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$image->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1);
|
||||
$precisionFix = -1;
|
||||
|
||||
$group->compositeImage($image, Imagick::COMPOSITE_OVER, 0, 0);
|
||||
$group->annotateImage($text, 32 + 4, $metrics['textHeight'] - $precisionFix, 0, $githubName);
|
||||
|
||||
$group->rotateImage(new ImagickPixel('#00000000'), 31.11);
|
||||
$x = 485 - $group->getImageWidth() / 2;
|
||||
$y = 530 - $group->getImageHeight() / 2;
|
||||
$baseImage->compositeImage($group, Imagick::COMPOSITE_OVER, $x, $y);
|
||||
} else {
|
||||
$text->skewY(20);
|
||||
$text->skewX(20);
|
||||
$text->setTextAlignment(\Imagick::ALIGN_CENTER);
|
||||
|
||||
$text->annotation(320 + 15 + 2, 640, $githubName);
|
||||
$metrics = $baseImage->queryFontMetrics($text, $githubName);
|
||||
|
||||
$image = new Imagick(__DIR__ . '/../../../public/images/cards/cloud/github-skew.png');
|
||||
$image->setGravity(Imagick::GRAVITY_CENTER);
|
||||
$baseImage->compositeImage($image, Imagick::COMPOSITE_OVER, 512 - ($metrics['textWidth'] / 2), 518 + \strlen($githubName) * 1.3);
|
||||
|
||||
$baseImage->drawImage($text);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($width) || !empty($height)) {
|
||||
$baseImage->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->file($baseImage->getImageBlob());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use Appwrite\Extend\Exception;
|
|||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
App::init()
|
||||
->groups(['console'])
|
||||
|
|
@ -28,13 +29,81 @@ App::get('/v1/console/variables')
|
|||
->label('sdk.response.model', Response::MODEL_CONSOLE_VARIABLES)
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
$isDomainEnabled = !empty(App::getEnv('_APP_DOMAIN', ''))
|
||||
&& !empty(App::getEnv('_APP_DOMAIN_TARGET', ''))
|
||||
&& App::getEnv('_APP_DOMAIN', '') !== 'localhost'
|
||||
&& App::getEnv('_APP_DOMAIN_TARGET', '') !== 'localhost';
|
||||
|
||||
$isVcsEnabled = !empty(App::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
|
||||
&& !empty(App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
|
||||
&& !empty(App::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
|
||||
&& !empty(App::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
|
||||
&& !empty(App::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''));
|
||||
|
||||
$isAssistantEnabled = !empty(App::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
|
||||
|
||||
$variables = new Document([
|
||||
'_APP_DOMAIN_TARGET' => App::getEnv('_APP_DOMAIN_TARGET'),
|
||||
'_APP_STORAGE_LIMIT' => +App::getEnv('_APP_STORAGE_LIMIT'),
|
||||
'_APP_FUNCTIONS_SIZE_LIMIT' => +App::getEnv('_APP_FUNCTIONS_SIZE_LIMIT'),
|
||||
'_APP_USAGE_STATS' => App::getEnv('_APP_USAGE_STATS'),
|
||||
'_APP_VCS_ENABLED' => $isVcsEnabled,
|
||||
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
|
||||
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled
|
||||
]);
|
||||
|
||||
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
|
||||
});
|
||||
|
||||
App::post('/v1/console/assistant')
|
||||
->desc('Ask Query')
|
||||
->groups(['api', 'assistant'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'assistant')
|
||||
->label('sdk.method', 'chat')
|
||||
->label('sdk.description', '/docs/references/assistant/chat.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_TEXT)
|
||||
->label('abuse-limit', 15)
|
||||
->label('abuse-key', 'userId:{userId}')
|
||||
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
|
||||
->inject('response')
|
||||
->action(function (string $prompt, Response $response) {
|
||||
$ch = curl_init('http://appwrite-assistant:3003/');
|
||||
$responseHeaders = [];
|
||||
$query = json_encode(['prompt' => $prompt]);
|
||||
$headers = ['accept: text/event-stream'];
|
||||
$handleEvent = function ($ch, $data) use ($response) {
|
||||
$response->chunk($data);
|
||||
|
||||
return \strlen($data);
|
||||
};
|
||||
|
||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $handleEvent);
|
||||
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 9000);
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
if (count($header) < 2) { // ignore invalid headers
|
||||
return $len;
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
});
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$response->chunk('', true);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,11 @@ use Appwrite\Event\Event;
|
|||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
|
|
@ -26,6 +30,7 @@ App::get('/v1/health')
|
|||
->action(function (Response $response) {
|
||||
|
||||
$output = [
|
||||
'name' => 'http',
|
||||
'status' => 'pass',
|
||||
'ping' => 0
|
||||
];
|
||||
|
|
@ -42,7 +47,6 @@ App::get('/v1/health/version')
|
|||
->label('sdk.response.model', Response::MODEL_HEALTH_VERSION)
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
|
||||
$response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION);
|
||||
});
|
||||
|
||||
|
|
@ -58,30 +62,50 @@ App::get('/v1/health/db')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
|
||||
->inject('response')
|
||||
->inject('utopia')
|
||||
->action(function (Response $response, App $utopia) {
|
||||
->inject('pools')
|
||||
->action(function (Response $response, Group $pools) {
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
$output = [];
|
||||
|
||||
try {
|
||||
$db = $utopia->getResource('db'); /* @var $db PDO */
|
||||
|
||||
// Run a small test to check the connection
|
||||
$statement = $db->prepare("SELECT 1;");
|
||||
|
||||
$statement->closeCursor();
|
||||
|
||||
$statement->execute();
|
||||
} catch (Exception $_e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Database is not available');
|
||||
}
|
||||
|
||||
$output = [
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
$configs = [
|
||||
'Console.DB' => Config::getParam('pools-console'),
|
||||
'Projects.DB' => Config::getParam('pools-database'),
|
||||
];
|
||||
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
foreach ($configs as $key => $config) {
|
||||
foreach ($config as $database) {
|
||||
try {
|
||||
$adapter = $pools->get($database)->pop()->getResource();
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
if ($adapter->ping()) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
} else {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'statuses' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_HEALTH_STATUS_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/health/cache')
|
||||
|
|
@ -96,23 +120,163 @@ App::get('/v1/health/cache')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
|
||||
->inject('response')
|
||||
->inject('utopia')
|
||||
->action(function (Response $response, App $utopia) {
|
||||
->inject('pools')
|
||||
->action(function (Response $response, Group $pools) {
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
$output = [];
|
||||
|
||||
$redis = $utopia->getResource('cache');
|
||||
|
||||
if (!$redis->ping(true)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Cache is not available');
|
||||
}
|
||||
|
||||
$output = [
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
$configs = [
|
||||
'Cache' => Config::getParam('pools-cache'),
|
||||
];
|
||||
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
foreach ($configs as $key => $config) {
|
||||
foreach ($config as $database) {
|
||||
try {
|
||||
$adapter = $pools->get($database)->pop()->getResource();
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
if ($adapter->ping()) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
} else {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'statuses' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_HEALTH_STATUS_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/health/queue')
|
||||
->desc('Get Queue')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getQueue')
|
||||
->label('sdk.description', '/docs/references/health/get-queue.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
|
||||
->inject('response')
|
||||
->inject('pools')
|
||||
->action(function (Response $response, Group $pools) {
|
||||
|
||||
$output = [];
|
||||
|
||||
$configs = [
|
||||
'Queue' => Config::getParam('pools-queue'),
|
||||
];
|
||||
|
||||
foreach ($configs as $key => $config) {
|
||||
foreach ($config as $database) {
|
||||
try {
|
||||
$adapter = $pools->get($database)->pop()->getResource();
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
if ($adapter->ping()) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
} else {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'statuses' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_HEALTH_STATUS_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/health/pubsub')
|
||||
->desc('Get PubSub')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getPubSub')
|
||||
->label('sdk.description', '/docs/references/health/get-pubsub.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
|
||||
->inject('response')
|
||||
->inject('pools')
|
||||
->action(function (Response $response, Group $pools) {
|
||||
|
||||
$output = [];
|
||||
|
||||
$configs = [
|
||||
'PubSub' => Config::getParam('pools-pubsub'),
|
||||
];
|
||||
|
||||
foreach ($configs as $key => $config) {
|
||||
foreach ($config as $database) {
|
||||
try {
|
||||
$adapter = $pools->get($database)->pop()->getResource();
|
||||
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
if ($adapter->ping()) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
} else {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$output[] = new Document([
|
||||
'name' => $key . " ($database)",
|
||||
'status' => 'fail',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'statuses' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_HEALTH_STATUS_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/health/time')
|
||||
|
|
@ -234,10 +398,11 @@ App::get('/v1/health/queue/functions')
|
|||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
|
||||
->inject('queue')
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
|
||||
$response->dynamic(new Document([ 'size' => Resque::size(Event::FUNCTIONS_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
|
||||
->action(function (Connection $queue, Response $response) {
|
||||
$client = new Client(Event::FUNCTIONS_QUEUE_NAME, $queue);
|
||||
$response->dynamic(new Document([ 'size' => $client->sumProcessingJobs() ]), Response::MODEL_HEALTH_QUEUE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/storage/local')
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ App::get('/v1/locale')
|
|||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_LOCALE)
|
||||
->label('sdk.offline.model', '/locale')
|
||||
->label('sdk.offline.model', '/localed')
|
||||
->label('sdk.offline.key', 'current')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
|
|
@ -68,6 +68,28 @@ App::get('/v1/locale')
|
|||
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
|
||||
});
|
||||
|
||||
App::get('/v1/locale/codes')
|
||||
->desc('List Locale Codes')
|
||||
->groups(['api', 'locale'])
|
||||
->label('scope', 'locale.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'locale')
|
||||
->label('sdk.method', 'listCodes')
|
||||
->label('sdk.description', '/docs/references/locale/list-locale-codes.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_LOCALE_CODE_LIST)
|
||||
->label('sdk.offline.model', '/locale/localeCode')
|
||||
->label('sdk.offline.key', 'current')
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
$codes = Config::getParam('locale-codes');
|
||||
$response->dynamic(new Document([
|
||||
'localeCodes' => $codes,
|
||||
'total' => count($codes),
|
||||
]), Response::MODEL_LOCALE_CODE_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/locale/countries')
|
||||
->desc('List Countries')
|
||||
->groups(['api', 'locale'])
|
||||
|
|
|
|||
989
app/controllers/api/migrations.php
Normal file
989
app/controllers/api/migrations.php
Normal file
|
|
@ -0,0 +1,989 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Host;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
include_once __DIR__ . '/../shared/api.php';
|
||||
|
||||
App::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Appwrite Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createAppwriteMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-appwrite.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
|
||||
->param('projectId', '', new UID(), "Source's Project ID")
|
||||
->param('apiKey', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'credentials' => [
|
||||
'endpoint' => $endpoint,
|
||||
'projectId' => $projectId,
|
||||
'apiKey' => $apiKey,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/firebase/oauth')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Firebase Data (OAuth)')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseOAuthMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->inject('request')
|
||||
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $events, Request $request) {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
if ($identity->getAttribute('secrets')) {
|
||||
$serviceAccount = $identity->getAttribute('secrets');
|
||||
} else {
|
||||
$firebase->cleanupServiceAccounts($accessToken, $projectId);
|
||||
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
|
||||
$identity = $identity
|
||||
->setAttribute('secrets', json_encode($serviceAccount));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Firebase::getName(),
|
||||
'credentials' => [
|
||||
'serviceAccount' => json_encode($serviceAccount),
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => []
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/firebase')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Firebase Data (Service Account)')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Firebase::getName(),
|
||||
'credentials' => [
|
||||
'serviceAccount' => $serviceAccount,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/supabase')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate Supabase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createSupabaseMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-supabase.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint')
|
||||
->param('apiKey', '', new Text(512), 'Source\'s API Key')
|
||||
->param('databaseHost', '', new Text(512), 'Source\'s Database Host')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Supabase::getName(),
|
||||
'credentials' => [
|
||||
'endpoint' => $endpoint,
|
||||
'apiKey' => $apiKey,
|
||||
'databaseHost' => $databaseHost,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'port' => $port,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::post('/v1/migrations/nhost')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Migrate NHost Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createNHostMigration')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-nhost.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('subdomain', '', new Text(512), 'Source\'s Subdomain')
|
||||
->param('region', '', new Text(512), 'Source\'s Region')
|
||||
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret')
|
||||
->param('database', '', new Text(512), 'Source\'s Database Name')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) {
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => NHost::getName(),
|
||||
'credentials' => [
|
||||
'subdomain' => $subdomain,
|
||||
'region' => $region,
|
||||
'adminSecret' => $adminSecret,
|
||||
'database' => $database,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'port' => $port,
|
||||
],
|
||||
'resources' => $resources,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
]));
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
// Trigger Transfer
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('List Migrations')
|
||||
->label('scope', 'migrations.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'list')
|
||||
->label('sdk.description', '/docs/references/migrations/list-migrations.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_LIST)
|
||||
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$migrationId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Migration '{$migrationId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'migrations' => $dbForProject->find('migrations', $queries),
|
||||
'total' => $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_MIGRATION_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Get Migration')
|
||||
->label('scope', 'migrations.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'get')
|
||||
->label('sdk.description', '/docs/references/migrations/get-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('migrationId', '', new UID(), 'Migration unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/appwrite/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Appwrite Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getAppwriteReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-appwrite-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
|
||||
->param('projectID', '', new Text(512), "Source's Project ID")
|
||||
->param('key', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($appwrite->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Firebase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getFirebaseReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $serviceAccount, Response $response) {
|
||||
try {
|
||||
$firebase = new Firebase(json_decode($serviceAccount, true));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($firebase->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/report/oauth')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Firebase Data using OAuth')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getFirebaseReportOAuth')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-firebase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
|
||||
->param('projectId', '', new Text(65536), 'Project ID')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('user')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
// Get Service Account
|
||||
if ($identity->getAttribute('secrets')) {
|
||||
$serviceAccount = $identity->getAttribute('secrets');
|
||||
} else {
|
||||
$firebase->cleanupServiceAccounts($accessToken, $projectId);
|
||||
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
|
||||
$identity = $identity
|
||||
->setAttribute('secrets', json_encode($serviceAccount));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$firebase = new Firebase($serviceAccount);
|
||||
|
||||
try {
|
||||
$report = $firebase->report($resources);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/connect')
|
||||
->desc('Authorize with firebase')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('origin', '*')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'createFirebaseAuth')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
|
||||
->label('sdk.methodType', 'webAuth')
|
||||
->label('sdk.hide', true)
|
||||
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
|
||||
->param('projectId', '', new UID(), 'Project ID')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('user')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
|
||||
$state = \json_encode([
|
||||
'projectId' => $projectId,
|
||||
'redirect' => $redirect,
|
||||
]);
|
||||
|
||||
$prefs = $user->getAttribute('prefs', []);
|
||||
$prefs['migrationState'] = $state;
|
||||
$user->setAttribute('prefs', $prefs);
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
$oauth2 = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
$url = $oauth2->getLoginURL();
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($url);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/redirect')
|
||||
->desc('Capture and receive data on Firebase authorization')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('error', __DIR__ . '/../../views/general/error.phtml')
|
||||
->param('code', '', new Text(2048), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
|
||||
$state = $user['prefs']['migrationState'] ?? '{}';
|
||||
$prefs['migrationState'] = '';
|
||||
$user->setAttribute('prefs', $prefs);
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
if (empty($state)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
|
||||
}
|
||||
|
||||
$state = \json_decode($state, true);
|
||||
$redirect = $state['redirect'] ?? '';
|
||||
$projectId = $state['projectId'] ?? '';
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if (empty($redirect)) {
|
||||
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
|
||||
}
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirect);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth Authroization
|
||||
if (!empty($code)) {
|
||||
$oauth2 = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$accessToken = $oauth2->getAccessToken($code);
|
||||
$refreshToken = $oauth2->getRefreshToken($code);
|
||||
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
|
||||
$email = $oauth2->getUserEmail($accessToken);
|
||||
$oauth2ID = $oauth2->getUserID($accessToken);
|
||||
|
||||
if (empty($accessToken)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
|
||||
}
|
||||
|
||||
if (empty($refreshToken)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
|
||||
}
|
||||
|
||||
if (empty($accessTokenExpiry)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
|
||||
if ($identity !== false && !$identity->isEmpty()) {
|
||||
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
if ($identity !== false && !$identity->isEmpty()) {
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
} else {
|
||||
$identity = $dbForConsole->createDocument('identities', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'userId' => $user->getId(),
|
||||
'provider' => 'firebase',
|
||||
'providerUid' => $oauth2ID,
|
||||
'providerEmail' => $email,
|
||||
'providerAccessToken' => $accessToken,
|
||||
'providerRefreshToken' => $refreshToken,
|
||||
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirect);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/projects')
|
||||
->desc('List Firebase Projects')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'listFirebaseProjects')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('request')
|
||||
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
|
||||
$firebase = new OAuth2Firebase(
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
|
||||
App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
|
||||
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
|
||||
);
|
||||
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $identity->getAttribute('providerAccessToken');
|
||||
$refreshToken = $identity->getAttribute('providerRefreshToken');
|
||||
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
|
||||
|
||||
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
|
||||
if ($isExpired) {
|
||||
try {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$accessToken = $firebase->getAccessToken('');
|
||||
$refreshToken = $firebase->getRefreshToken('');
|
||||
|
||||
$verificationId = $firebase->getUserID($accessToken);
|
||||
|
||||
if (empty($verificationId)) {
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
|
||||
}
|
||||
|
||||
$identity = $identity
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
|
||||
|
||||
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
|
||||
}
|
||||
|
||||
$projects = $firebase->getProjects($accessToken);
|
||||
|
||||
$output = [];
|
||||
foreach ($projects as $project) {
|
||||
$output[] = [
|
||||
'displayName' => $project['displayName'],
|
||||
'projectId' => $project['projectId'],
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'projects' => $output,
|
||||
'total' => count($output),
|
||||
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/firebase/deauthorize')
|
||||
->desc('Revoke Appwrite\'s authorization to access Firebase Projects')
|
||||
->groups(['api', 'migrations'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'deleteFirebaseAuth')
|
||||
->label('sdk.description', '')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (Document $user, Response $response, Database $dbForConsole) {
|
||||
$identity = $dbForConsole->findOne('identities', [
|
||||
Query::equal('provider', ['firebase']),
|
||||
Query::equal('userInternalId', [$user->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($identity === false || $identity->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
|
||||
}
|
||||
|
||||
$dbForConsole->deleteDocument('identities', $identity->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/supabase/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on Supabase Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getSupabaseReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-supabase-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
|
||||
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint.')
|
||||
->param('apiKey', '', new Text(512), 'Source\'s API Key.')
|
||||
->param('databaseHost', '', new Text(512), 'Source\'s Database Host.')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username.')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password.')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
|
||||
try {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($supabase->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::get('/v1/migrations/nhost/report')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Generate a report on NHost Data')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'getNHostReport')
|
||||
->label('sdk.description', '/docs/references/migrations/migration-nhost-report.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION_REPORT)
|
||||
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate.')
|
||||
->param('subdomain', '', new Text(512), 'Source\'s Subdomain.')
|
||||
->param('region', '', new Text(512), 'Source\'s Region.')
|
||||
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret.')
|
||||
->param('database', '', new Text(512), 'Source\'s Database Name.')
|
||||
->param('username', '', new Text(512), 'Source\'s Database Username.')
|
||||
->param('password', '', new Text(512), 'Source\'s Database Password.')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
|
||||
try {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document($nhost->report($resources)), Response::MODEL_MIGRATION_REPORT);
|
||||
} catch (\Exception $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
App::patch('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Retry Migration')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].retry')
|
||||
->label('audits.event', 'migration.retry')
|
||||
->label('audits.resource', 'migrations/{request.migrationId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'retry')
|
||||
->label('sdk.description', '/docs/references/migrations/retry-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MIGRATION)
|
||||
->param('migrationId', '', new UID(), 'Migration unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventInstance) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($migration->getAttribute('status') !== 'failed') {
|
||||
throw new Exception(Exception::MIGRATION_IN_PROGRESS, 'Migration not failed yet');
|
||||
}
|
||||
|
||||
$migration
|
||||
->setAttribute('status', 'pending')
|
||||
->setAttribute('dateUpdated', \time());
|
||||
|
||||
// Trigger Migration
|
||||
$event = new Migration();
|
||||
$event
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::delete('/v1/migrations/:migrationId')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Delete Migration')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].delete')
|
||||
->label('audits.event', 'migrationId.delete')
|
||||
->label('audits.resource', 'migrations/{request.migrationId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'migrations')
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/functions/delete-migration.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('migrationId', '', new UID(), 'Migration ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
->action(function (string $migrationId, Response $response, Database $dbForProject, Event $events) {
|
||||
$migration = $dbForProject->getDocument('migrations', $migrationId);
|
||||
|
||||
if ($migration->isEmpty()) {
|
||||
throw new Exception(Exception::MIGRATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('migrations', $migration->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB');
|
||||
}
|
||||
|
||||
$events->setParam('migrationId', $migration->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
311
app/controllers/api/project.php
Normal file
311
app/controllers/api/project.php
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Database\DateTime;
|
||||
|
||||
App::get('/v1/project/usage')
|
||||
->desc('Get usage stats for a project')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'getUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_PROJECT)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $range, Response $response, Database $dbForProject) {
|
||||
$usage = [];
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
|
||||
$periods = [
|
||||
'24h' => [
|
||||
'period' => '1h',
|
||||
'limit' => 24,
|
||||
],
|
||||
'7d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 7,
|
||||
],
|
||||
'30d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 30,
|
||||
],
|
||||
'90d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 90,
|
||||
],
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
'project.$all.network.requests',
|
||||
'project.$all.network.bandwidth',
|
||||
'project.$all.storage.size',
|
||||
'users.$all.count.total',
|
||||
'databases.$all.count.total',
|
||||
'documents.$all.count.total',
|
||||
'executions.$all.compute.total',
|
||||
'buckets.$all.count.total'
|
||||
];
|
||||
|
||||
$stats = [];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$limit = $periods[$range]['limit'];
|
||||
$period = $periods[$range]['period'];
|
||||
|
||||
$requestDocs = $dbForProject->find('stats', [
|
||||
Query::equal('period', [$period]),
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
|
||||
$stats[$metric] = [];
|
||||
foreach ($requestDocs as $requestDoc) {
|
||||
$stats[$metric][] = [
|
||||
'value' => $requestDoc->getAttribute('value'),
|
||||
'date' => $requestDoc->getAttribute('time'),
|
||||
];
|
||||
}
|
||||
|
||||
// backfill metrics with empty values for graphs
|
||||
$backfill = $limit - \count($requestDocs);
|
||||
while ($backfill > 0) {
|
||||
$last = $limit - $backfill - 1; // array index of last added metric
|
||||
$diff = match ($period) { // convert period to seconds for unix timestamp math
|
||||
'1h' => 3600,
|
||||
'1d' => 86400,
|
||||
};
|
||||
$stats[$metric][] = [
|
||||
'value' => 0,
|
||||
'date' => DateTime::formatTz(DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff)),
|
||||
];
|
||||
$backfill--;
|
||||
}
|
||||
$stats[$metric] = array_reverse($stats[$metric]);
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'requests' => $stats[$metrics[0]] ?? [],
|
||||
'network' => $stats[$metrics[1]] ?? [],
|
||||
'storage' => $stats[$metrics[2]] ?? [],
|
||||
'users' => $stats[$metrics[3]] ?? [],
|
||||
'databases' => $stats[$metrics[4]] ?? [],
|
||||
'documents' => $stats[$metrics[5]] ?? [],
|
||||
'executions' => $stats[$metrics[6]] ?? [],
|
||||
'buckets' => $stats[$metrics[7]] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_PROJECT);
|
||||
});
|
||||
|
||||
// Variables
|
||||
|
||||
App::post('/v1/project/variables')
|
||||
->desc('Create Variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'createVariable')
|
||||
->label('sdk.description', '/docs/references/project/create-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
$variableId = ID::unique();
|
||||
|
||||
$variable = new Document([
|
||||
'$id' => $variableId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => '',
|
||||
'resourceId' => '',
|
||||
'resourceType' => 'project',
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'search' => implode(' ', [$variableId, $key, 'project']),
|
||||
]);
|
||||
|
||||
try {
|
||||
$variable = $dbForProject->createDocument('variables', $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
App::get('/v1/project/variables')
|
||||
->desc('List Variables')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'listVariables')
|
||||
->label('sdk.description', '/docs/references/project/list-variables.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE_LIST)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (Response $response, Database $dbForProject) {
|
||||
$variables = $dbForProject->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'variables' => $variables,
|
||||
'total' => \count($variables),
|
||||
]), Response::MODEL_VARIABLE_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/project/variables/:variableId')
|
||||
->desc('Get Variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'getVariable')
|
||||
->label('sdk.description', '/docs/references/project/get-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
App::put('/v1/project/variables/:variableId')
|
||||
->desc('Update Variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'updateVariable')
|
||||
->label('sdk.description', '/docs/references/project/update-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variable
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('value', $value ?? $variable->getAttribute('value'))
|
||||
->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$dbForProject->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
App::delete('/v1/project/variables/:variableId')
|
||||
->desc('Delete Variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
->label('sdk.method', 'deleteVariable')
|
||||
->label('sdk.description', '/docs/references/project/delete-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
$dbForProject->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
|
@ -1,40 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Network\Validator\CNAME;
|
||||
use Utopia\Validator\Domain as DomainValidator;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Utopia\Validator\URL;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\ProjectId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::init()
|
||||
|
|
@ -56,10 +58,10 @@ App::post('/v1/projects')
|
|||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
|
||||
->param('teamId', '', new UID(), 'Team unique ID.')
|
||||
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn($config) => !$config['disabled']))), 'Project Region.', true)
|
||||
->param('region', App::getEnv('_APP_REGION', 'default'), new Whitelist(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true)
|
||||
->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true)
|
||||
->param('logo', '', new Text(1024), 'Project logo.', true)
|
||||
->param('url', '', new URL(), 'Project URL.', true)
|
||||
|
|
@ -71,8 +73,10 @@ App::post('/v1/projects')
|
|||
->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Database $dbForProject) {
|
||||
->inject('cache')
|
||||
->inject('pools')
|
||||
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Cache $cache, Group $pools) {
|
||||
|
||||
|
||||
$team = $dbForConsole->getDocument('teams', $teamId);
|
||||
|
||||
|
|
@ -81,13 +85,47 @@ App::post('/v1/projects')
|
|||
}
|
||||
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
|
||||
foreach ($auth as $index => $method) {
|
||||
$auths[$method['key'] ?? ''] = true;
|
||||
}
|
||||
|
||||
$projectId = ($projectId == 'unique()') ? ID::unique() : $projectId;
|
||||
|
||||
$backups['database_db_fra1_02'] = ['from' => '7:30', 'to' => '8:15'];
|
||||
$backups['database_db_fra1_03'] = ['from' => '10:30', 'to' => '11:15'];
|
||||
$backups['database_db_fra1_04'] = ['from' => '13:30', 'to' => '14:15'];
|
||||
$backups['database_db_fra1_05'] = ['from' => '4:30', 'to' => '5:15'];
|
||||
$backups['database_db_fra1_06'] = ['from' => '16:30', 'to' => '17:15'];
|
||||
|
||||
$databases = Config::getParam('pools-database', []);
|
||||
|
||||
/**
|
||||
* Extract db from list while backing
|
||||
*/
|
||||
if (count($databases) > 1) {
|
||||
$now = new \DateTime();
|
||||
|
||||
foreach ($databases as $index => $database) {
|
||||
if (empty($backups[$database])) {
|
||||
continue;
|
||||
}
|
||||
$backup = $backups[$database];
|
||||
$from = \DateTime::createFromFormat('H:i', $backup['from']);
|
||||
$to = \DateTime::createFromFormat('H:i', $backup['to']);
|
||||
if ($now >= $from && $now <= $to) {
|
||||
unset($databases[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($index = array_search('database_db_fra1_06', $databases)) {
|
||||
$database = $databases[$index];
|
||||
} else {
|
||||
$database = $databases[array_rand($databases)];
|
||||
}
|
||||
|
||||
if ($projectId === 'console') {
|
||||
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
|
||||
}
|
||||
|
|
@ -121,17 +159,15 @@ App::post('/v1/projects')
|
|||
'authProviders' => [],
|
||||
'webhooks' => null,
|
||||
'keys' => null,
|
||||
'domains' => null,
|
||||
'auths' => $auths,
|
||||
'search' => implode(' ', [$projectId, $name]),
|
||||
'database' => $database
|
||||
]));
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
|
||||
$dbForProject = new Database($pools->get($database)->pop()->getResource(), $cache);
|
||||
$dbForProject->setNamespace("_{$project->getInternalId()}");
|
||||
$dbForProject->create();
|
||||
|
||||
|
|
@ -141,6 +177,9 @@ App::post('/v1/projects')
|
|||
$adapter = new TimeLimit('', 0, 1, $dbForProject);
|
||||
$adapter->setup();
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', [])['projects'] ?? [];
|
||||
|
||||
foreach ($collections as $key => $collection) {
|
||||
if (($collection['$collection'] ?? '') !== Database::METADATA) {
|
||||
continue;
|
||||
|
|
@ -203,7 +242,9 @@ App::get('/v1/projects')
|
|||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -395,17 +436,57 @@ App::patch('/v1/projects/:projectId')
|
|||
}
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('description', $description)
|
||||
->setAttribute('logo', $logo)
|
||||
->setAttribute('url', $url)
|
||||
->setAttribute('legalName', $legalName)
|
||||
->setAttribute('legalCountry', $legalCountry)
|
||||
->setAttribute('legalState', $legalState)
|
||||
->setAttribute('legalCity', $legalCity)
|
||||
->setAttribute('legalAddress', $legalAddress)
|
||||
->setAttribute('legalTaxId', $legalTaxId)
|
||||
->setAttribute('search', implode(' ', [$projectId, $name])));
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('description', $description)
|
||||
->setAttribute('logo', $logo)
|
||||
->setAttribute('url', $url)
|
||||
->setAttribute('legalName', $legalName)
|
||||
->setAttribute('legalCountry', $legalCountry)
|
||||
->setAttribute('legalState', $legalState)
|
||||
->setAttribute('legalCity', $legalCity)
|
||||
->setAttribute('legalAddress', $legalAddress)
|
||||
->setAttribute('legalTaxId', $legalTaxId)
|
||||
->setAttribute('search', implode(' ', [$projectId, $name])));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/team')
|
||||
->desc('Update Project Team')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updateTeam')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('teamId', '', new UID(), 'Team ID of the team to transfer project to.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $teamId, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
$team = $dbForConsole->getDocument('teams', $teamId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($team->isEmpty()) {
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('teamId', $teamId)
|
||||
->setAttribute('$permissions', [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
]));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
|
@ -421,7 +502,7 @@ App::patch('/v1/projects/:projectId/service')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn($element) => $element['optional'])), true), 'Service name.')
|
||||
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
|
|
@ -441,6 +522,40 @@ App::patch('/v1/projects/:projectId/service')
|
|||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/service/all')
|
||||
->desc('Update all service status')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updateServiceStatusAll')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('status', null, new Boolean(), 'Service status.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, bool $status, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
|
||||
|
||||
$services = [];
|
||||
foreach ($allServices as $service) {
|
||||
$services[$service] = $status;
|
||||
}
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/oauth2')
|
||||
->desc('Update Project OAuth2')
|
||||
->groups(['api', 'projects'])
|
||||
|
|
@ -613,7 +728,7 @@ App::patch('/v1/projects/:projectId/auth/password-history')
|
|||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
||||
->desc('Update authentication password disctionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||
->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
|
|
@ -643,6 +758,37 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
|||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/personal-data')
|
||||
->desc('Enable or disable checking user passwords for similarity with their personal data.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updatePersonalDataCheck')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not to check a password for similarity with personal data. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['personalDataCheck'] = $enabled;
|
||||
|
||||
$dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
||||
->desc('Update Project user sessions limit')
|
||||
->groups(['api', 'projects'])
|
||||
|
|
@ -697,8 +843,7 @@ App::delete('/v1/projects/:projectId')
|
|||
|
||||
$deletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($project)
|
||||
;
|
||||
->setDocument($project);
|
||||
|
||||
if (!$dbForConsole->deleteDocument('projects', $projectId)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB');
|
||||
|
|
@ -820,7 +965,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
}
|
||||
|
||||
$webhook = $dbForConsole->findOne('webhooks', [
|
||||
Query::equal('_uid', [$webhookId]),
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -862,7 +1007,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
|
||||
|
||||
$webhook = $dbForConsole->findOne('webhooks', [
|
||||
Query::equal('_uid', [$webhookId]),
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -876,8 +1021,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
->setAttribute('url', $url)
|
||||
->setAttribute('security', $security)
|
||||
->setAttribute('httpUser', $httpUser)
|
||||
->setAttribute('httpPass', $httpPass)
|
||||
;
|
||||
->setAttribute('httpPass', $httpPass);
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
|
@ -908,7 +1052,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
|||
}
|
||||
|
||||
$webhook = $dbForConsole->findOne('webhooks', [
|
||||
Query::equal('_uid', [$webhookId]),
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -946,7 +1090,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
}
|
||||
|
||||
$webhook = $dbForConsole->findOne('webhooks', [
|
||||
Query::equal('_uid', [$webhookId]),
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1068,7 +1212,7 @@ App::get('/v1/projects/:projectId/keys/:keyId')
|
|||
}
|
||||
|
||||
$key = $dbForConsole->findOne('keys', [
|
||||
Query::equal('_uid', [$keyId]),
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1105,7 +1249,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
|||
}
|
||||
|
||||
$key = $dbForConsole->findOne('keys', [
|
||||
Query::equal('_uid', [$keyId]),
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1116,8 +1260,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
|||
$key
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('scopes', $scopes)
|
||||
->setAttribute('expire', $expire)
|
||||
;
|
||||
->setAttribute('expire', $expire);
|
||||
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
|
||||
|
|
@ -1148,7 +1291,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
|
|||
}
|
||||
|
||||
$key = $dbForConsole->findOne('keys', [
|
||||
Query::equal('_uid', [$keyId]),
|
||||
Query::equal('$id', [$keyId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1270,7 +1413,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
|
|||
}
|
||||
|
||||
$platform = $dbForConsole->findOne('platforms', [
|
||||
Query::equal('_uid', [$platformId]),
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1307,7 +1450,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
|
|||
}
|
||||
|
||||
$platform = $dbForConsole->findOne('platforms', [
|
||||
Query::equal('_uid', [$platformId]),
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1319,8 +1462,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
|
|||
->setAttribute('name', $name)
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('store', $store)
|
||||
->setAttribute('hostname', $hostname)
|
||||
;
|
||||
->setAttribute('hostname', $hostname);
|
||||
|
||||
$dbForConsole->updateDocument('platforms', $platform->getId(), $platform);
|
||||
|
||||
|
|
@ -1351,7 +1493,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
|
|||
}
|
||||
|
||||
$platform = $dbForConsole->findOne('platforms', [
|
||||
Query::equal('_uid', [$platformId]),
|
||||
Query::equal('$id', [$platformId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
|
|
@ -1366,87 +1508,30 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
|
|||
$response->noContent();
|
||||
});
|
||||
|
||||
// Domains
|
||||
|
||||
App::post('/v1/projects/:projectId/domains')
|
||||
->desc('Create Domain')
|
||||
// CUSTOM SMTP and Templates
|
||||
App::patch('/v1/projects/:projectId/smtp')
|
||||
->desc('Update SMTP configuration')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'createDomain')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DOMAIN)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('domain', null, new DomainValidator(), 'Domain name.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $domain, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$document = $dbForConsole->findOne('domains', [
|
||||
Query::equal('domain', [$domain]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($document && !$document->isEmpty()) {
|
||||
throw new Exception(Exception::DOMAIN_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
|
||||
|
||||
if (!$target->isKnown() || $target->isTest()) {
|
||||
throw new Exception(Exception::DOMAIN_TARGET_INVALID, 'Unreachable CNAME target (' . $target->get() . '). Please check the _APP_DOMAIN_TARGET environment variable of your Appwrite server.');
|
||||
}
|
||||
|
||||
$domain = new Domain($domain);
|
||||
|
||||
$domain = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'updated' => DateTime::now(),
|
||||
'domain' => $domain->get(),
|
||||
'tld' => $domain->getSuffix(),
|
||||
'registerable' => $domain->getRegisterable(),
|
||||
'verification' => false,
|
||||
'certificateId' => null,
|
||||
]);
|
||||
|
||||
$domain = $dbForConsole->createDocument('domains', $domain);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($domain, Response::MODEL_DOMAIN);
|
||||
});
|
||||
|
||||
App::get('/v1/projects/:projectId/domains')
|
||||
->desc('List Domains')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'listDomains')
|
||||
->label('sdk.method', 'updateSmtpConfiguration')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DOMAIN_LIST)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('enabled', false, new Boolean(), 'Enable custom SMTP service')
|
||||
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
|
||||
->param('senderEmail', '', new Email(), 'Email of the sender', true)
|
||||
->param('replyTo', '', new Email(), 'Reply to email', true)
|
||||
->param('host', '', new HostName(), 'SMTP server host name', true)
|
||||
->param('port', 587, new Integer(), 'SMTP server port', true)
|
||||
->param('username', '', new Text(0, 0), 'SMTP server username', true)
|
||||
->param('password', '', new Text(0, 0), 'SMTP server password', true)
|
||||
->param('secure', '', new WhiteList(['tls'], true), 'Does SMTP server use secure connection', true)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, Response $response, Database $dbForConsole) {
|
||||
->action(function (string $projectId, bool $enabled, string $senderName, string $senderEmail, string $replyTo, string $host, int $port, string $username, string $password, string $secure, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
|
@ -1454,66 +1539,220 @@ App::get('/v1/projects/:projectId/domains')
|
|||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$domains = $dbForConsole->find('domains', [
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
// Ensure required params for when enabling SMTP
|
||||
if ($enabled) {
|
||||
if (empty($senderName)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender name is required when enabling SMTP.');
|
||||
} elseif (empty($senderEmail)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Sender email is required when enabling SMTP.');
|
||||
} elseif (empty($host)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Host is required when enabling SMTP.');
|
||||
} elseif (empty($port)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Port is required when enabling SMTP.');
|
||||
}
|
||||
}
|
||||
|
||||
// validate SMTP settings
|
||||
if ($enabled) {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->Host = $host;
|
||||
$mail->Port = $port;
|
||||
$mail->SMTPSecure = $secure;
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->Timeout = 5;
|
||||
|
||||
try {
|
||||
$valid = $mail->SmtpConnect();
|
||||
|
||||
if (!$valid) {
|
||||
throw new Exception('Connection is not valid.');
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
throw new Exception(Exception::PROJECT_SMTP_CONFIG_INVALID, 'Could not connect to SMTP server: ' . $error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Save SMTP settings
|
||||
if ($enabled) {
|
||||
$smtp = [
|
||||
'enabled' => $enabled,
|
||||
'senderName' => $senderName,
|
||||
'senderEmail' => $senderEmail,
|
||||
'replyTo' => $replyTo,
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'secure' => $secure,
|
||||
];
|
||||
} else {
|
||||
$smtp = [
|
||||
'enabled' => false
|
||||
];
|
||||
}
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('smtp', $smtp));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Get custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'getSmsTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
$template = [
|
||||
'message' => Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl')->render(),
|
||||
];
|
||||
}
|
||||
|
||||
$template['type'] = $type;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
App::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Get custom email template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'getEmailTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_EMAIL_TEMPLATE)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
$localeObj = new Locale($locale);
|
||||
if (is_null($template)) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
|
||||
->setParam('{{name}}', '')
|
||||
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'))
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
|
||||
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
|
||||
$message = $message->render();
|
||||
|
||||
$template = [
|
||||
'message' => $message,
|
||||
'subject' => $localeObj->getText('emails.' . $type . '.subject'),
|
||||
'senderEmail' => '',
|
||||
'senderName' => ''
|
||||
];
|
||||
}
|
||||
|
||||
$template['type'] = $type;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Update custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updateSmsTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $type, string $locale, string $message, Response $response, Database $dbForConsole) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$templates['sms.' . $type . '-' . $locale] = [
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'domains' => $domains,
|
||||
'total' => count($domains),
|
||||
]), Response::MODEL_DOMAIN_LIST);
|
||||
'message' => $message,
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
App::get('/v1/projects/:projectId/domains/:domainId')
|
||||
->desc('Get Domain')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'getDomain')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DOMAIN)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('domainId', '', new UID(), 'Domain unique ID.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$domain = $dbForConsole->findOne('domains', [
|
||||
Query::equal('_uid', [$domainId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
|
||||
if ($domain === false || $domain->isEmpty()) {
|
||||
throw new Exception(Exception::DOMAIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($domain, Response::MODEL_DOMAIN);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/domains/:domainId/verification')
|
||||
->desc('Update Domain Verification Status')
|
||||
App::patch('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Update custom email templates')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updateDomainVerification')
|
||||
->label('sdk.method', 'updateEmailTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DOMAIN)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('domainId', '', new UID(), 'Domain unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->param('subject', '', new Text(255), 'Email Subject')
|
||||
->param('message', '', new Text(0), 'Template message')
|
||||
->param('senderName', '', new Text(255, 0), 'Name of the email sender', true)
|
||||
->param('senderEmail', '', new Email(), 'Email of the sender', true)
|
||||
->param('replyTo', '', new Email(), 'Reply to email', true)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
|
||||
->action(function (string $projectId, string $type, string $locale, string $subject, string $message, string $senderName, string $senderEmail, string $replyTo, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
|
@ -1521,59 +1760,46 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
|
|||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$domain = $dbForConsole->findOne('domains', [
|
||||
Query::equal('_uid', [$domainId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$templates['email.' . $type . '-' . $locale] = [
|
||||
'senderName' => $senderName,
|
||||
'senderEmail' => $senderEmail,
|
||||
'subject' => $subject,
|
||||
'replyTo' => $replyTo,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
if ($domain === false || $domain->isEmpty()) {
|
||||
throw new Exception(Exception::DOMAIN_NOT_FOUND);
|
||||
}
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
|
||||
|
||||
if (!$target->isKnown() || $target->isTest()) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
|
||||
}
|
||||
|
||||
if ($domain->getAttribute('verification') === true) {
|
||||
return $response->dynamic($domain, Response::MODEL_DOMAIN);
|
||||
}
|
||||
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
|
||||
if (!$validator->isValid($domain->getAttribute('domain', ''))) {
|
||||
throw new Exception(Exception::DOMAIN_VERIFICATION_FAILED);
|
||||
}
|
||||
|
||||
|
||||
$dbForConsole->updateDocument('domains', $domain->getId(), $domain->setAttribute('verification', true));
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
// Issue a TLS certificate when domain is verified
|
||||
$event = new Certificate();
|
||||
$event
|
||||
->setDomain($domain)
|
||||
->trigger();
|
||||
|
||||
$response->dynamic($domain, Response::MODEL_DOMAIN);
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
'senderName' => $senderName,
|
||||
'senderEmail' => $senderEmail,
|
||||
'subject' => $subject,
|
||||
'replyTo' => $replyTo,
|
||||
'message' => $message
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
||||
App::delete('/v1/projects/:projectId/domains/:domainId')
|
||||
->desc('Delete Domain')
|
||||
App::delete('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
->desc('Reset custom SMS template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'deleteDomain')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->label('sdk.method', 'deleteSmsTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SMS_TEMPLATE)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('domainId', '', new UID(), 'Domain unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['sms'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->inject('deletes')
|
||||
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole, Delete $deletes) {
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
|
||||
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
|
@ -1581,22 +1807,65 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
|
|||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$domain = $dbForConsole->findOne('domains', [
|
||||
Query::equal('_uid', [$domainId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
]);
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['sms.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if ($domain === false || $domain->isEmpty()) {
|
||||
throw new Exception(Exception::DOMAIN_NOT_FOUND);
|
||||
if (is_null($template)) {
|
||||
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
|
||||
}
|
||||
|
||||
$dbForConsole->deleteDocument('domains', $domain->getId());
|
||||
unset($template['sms.' . $type . '-' . $locale]);
|
||||
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$deletes
|
||||
->setType(DELETE_TYPE_CERTIFICATES)
|
||||
->setDocument($domain);
|
||||
|
||||
$response->noContent();
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
'message' => $template['message']
|
||||
]), Response::MODEL_SMS_TEMPLATE);
|
||||
});
|
||||
|
||||
App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
->desc('Reset custom email template')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'deleteEmailTemplate')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_EMAIL_TEMPLATE)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('type', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? []), 'Template type')
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale', false, ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, string $type, string $locale, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $type . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
|
||||
}
|
||||
|
||||
unset($templates['email.' . $type . '-' . $locale]);
|
||||
|
||||
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('templates', $templates));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
'locale' => $locale,
|
||||
'senderName' => $template['senderName'],
|
||||
'senderEmail' => $template['senderEmail'],
|
||||
'subject' => $template['subject'],
|
||||
'replyTo' => $template['replyTo'],
|
||||
'message' => $template['message']
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
});
|
||||
|
|
|
|||
316
app/controllers/api/proxy.php
Normal file
316
app/controllers/api/proxy.php
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\CNAME;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Rules;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Validator\Domain as ValidatorDomain;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::post('/v1/proxy/rules')
|
||||
->groups(['api', 'proxy'])
|
||||
->desc('Create Rule')
|
||||
->label('scope', 'rules.write')
|
||||
->label('event', 'rules.[ruleId].create')
|
||||
->label('audits.event', 'rule.create')
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'createRule')
|
||||
->label('sdk.description', '/docs/references/proxy/create-rule.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
|
||||
->param('domain', null, new ValidatorDomain(), 'Domain name.')
|
||||
->param('resourceType', null, new WhiteList(['api', 'function']), 'Action definition for the rule. Possible values are "api", "function"')
|
||||
->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('events')
|
||||
->inject('dbForConsole')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Event $events, Database $dbForConsole, Database $dbForProject) {
|
||||
$mainDomain = App::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.');
|
||||
}
|
||||
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
|
||||
}
|
||||
|
||||
$document = $dbForConsole->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]);
|
||||
|
||||
if ($document && !$document->isEmpty()) {
|
||||
if ($document->getAttribute('projectId') === $project->getId()) {
|
||||
$resourceType = $document->getAttribute('resourceType');
|
||||
$resourceId = $document->getAttribute('resourceId');
|
||||
$message = "Domain already assigned to '{$resourceType}' service";
|
||||
if (!empty($resourceId)) {
|
||||
$message .= " with ID '{$resourceId}'";
|
||||
}
|
||||
|
||||
$message .= '.';
|
||||
} else {
|
||||
$message = 'Domain already assigned to different project.';
|
||||
}
|
||||
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS, $message);
|
||||
}
|
||||
|
||||
$resourceInternalId = '';
|
||||
|
||||
if ($resourceType == 'function') {
|
||||
if (empty($resourceId)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $resourceId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$resourceInternalId = $function->getInternalId();
|
||||
}
|
||||
|
||||
$domain = new Domain($domain);
|
||||
|
||||
$ruleId = ID::unique();
|
||||
$rule = new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain->get(),
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceInternalId' => $resourceInternalId,
|
||||
'certificateId' => '',
|
||||
]);
|
||||
|
||||
$status = 'created';
|
||||
$functionsDomain = App::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
if (!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) {
|
||||
$status = 'verified';
|
||||
}
|
||||
|
||||
if ($status === 'created') {
|
||||
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
|
||||
if ($validator->isValid($domain->get())) {
|
||||
$status = 'verifying';
|
||||
|
||||
$event = new Certificate();
|
||||
$event
|
||||
->setDomain(new Document([
|
||||
'domain' => $rule->getAttribute('domain')
|
||||
]))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
$rule->setAttribute('status', $status);
|
||||
$rule = $dbForConsole->createDocument('rules', $rule);
|
||||
|
||||
$events->setParam('ruleId', $rule->getId());
|
||||
|
||||
$rule->setAttribute('logs', '');
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
});
|
||||
|
||||
App::get('/v1/proxy/rules')
|
||||
->groups(['api', 'proxy'])
|
||||
->desc('List Rules')
|
||||
->label('scope', 'rules.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'listRules')
|
||||
->label('sdk.description', '/docs/references/proxy/list-rules.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROXY_RULE_LIST)
|
||||
->param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getInternalId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$ruleId = $cursor->getValue();
|
||||
$cursorDocument = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$rules = $dbForConsole->find('rules', $queries);
|
||||
foreach ($rules as $rule) {
|
||||
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
|
||||
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
|
||||
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'rules' => $rules,
|
||||
'total' => $dbForConsole->count('rules', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_PROXY_RULE_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/proxy/rules/:ruleId')
|
||||
->groups(['api', 'proxy'])
|
||||
->desc('Get Rule')
|
||||
->label('scope', 'rules.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'getRule')
|
||||
->label('sdk.description', '/docs/references/proxy/get-rule.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
|
||||
->param('ruleId', '', new UID(), 'Rule ID.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
|
||||
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
|
||||
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
|
||||
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
});
|
||||
|
||||
App::delete('/v1/proxy/rules/:ruleId')
|
||||
->groups(['api', 'proxy'])
|
||||
->desc('Delete Rule')
|
||||
->label('scope', 'rules.write')
|
||||
->label('event', 'rules.[ruleId].delete')
|
||||
->label('audits.event', 'rules.delete')
|
||||
->label('audits.resource', 'rule/{request.ruleId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'deleteRule')
|
||||
->label('sdk.description', '/docs/references/proxy/delete-rule.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('ruleId', '', new UID(), 'Rule ID.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('deletes')
|
||||
->inject('events')
|
||||
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole, Delete $deletes, Event $events) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForConsole->deleteDocument('rules', $rule->getId());
|
||||
|
||||
$deletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($rule);
|
||||
|
||||
$events->setParam('ruleId', $rule->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
->desc('Update Rule Verification Status')
|
||||
->groups(['api', 'proxy'])
|
||||
->label('scope', 'rules.write')
|
||||
->label('event', 'rules.[ruleId].update')
|
||||
->label('audits.event', 'rule.update')
|
||||
->label('audits.resource', 'rule/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'updateRuleVerification')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
|
||||
->param('ruleId', '', new UID(), 'Rule ID.')
|
||||
->inject('response')
|
||||
->inject('events')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $ruleId, Response $response, Event $events, Document $project, Database $dbForConsole) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
throw new Exception(Exception::RULE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
|
||||
|
||||
if (!$target->isKnown() || $target->isTest()) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Domain target must be configured as environment variable.');
|
||||
}
|
||||
|
||||
if ($rule->getAttribute('verification') === true) {
|
||||
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
}
|
||||
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
$domain = new Domain($rule->getAttribute('domain', ''));
|
||||
|
||||
if (!$validator->isValid($domain->get())) {
|
||||
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
||||
}
|
||||
|
||||
$dbForConsole->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
|
||||
|
||||
// Issue a TLS certificate when domain is verified
|
||||
$event = new Certificate();
|
||||
$event
|
||||
->setDomain(new Document([
|
||||
'domain' => $rule->getAttribute('domain')
|
||||
]))
|
||||
->trigger();
|
||||
|
||||
$events->setParam('ruleId', $rule->getId());
|
||||
|
||||
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
|
||||
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
|
||||
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Hash\Sha;
|
||||
use Appwrite\ClamAV\Network;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
|
|
@ -41,6 +42,7 @@ use Utopia\Validator\HexColor;
|
|||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Swoole\Request;
|
||||
|
||||
App::post('/v1/storage/buckets')
|
||||
|
|
@ -62,8 +64,8 @@ App::post('/v1/storage/buckets')
|
|||
->param('name', '', new Text(128), 'Bucket name')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
|
||||
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](/docs/environment-variables#storage)', true)
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
|
||||
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true)
|
||||
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
|
||||
->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
|
|
@ -79,7 +81,7 @@ App::post('/v1/storage/buckets')
|
|||
$permissions = Permission::aggregate($permissions);
|
||||
|
||||
try {
|
||||
$files = Config::getParam('collections', [])['files'] ?? [];
|
||||
$files = (Config::getParam('collections', [])['buckets'] ?? [])['files'] ?? [];
|
||||
if (empty($files)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Files collection is not configured.');
|
||||
}
|
||||
|
|
@ -167,7 +169,9 @@ App::get('/v1/storage/buckets')
|
|||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -234,8 +238,8 @@ App::put('/v1/storage/buckets/:bucketId')
|
|||
->param('name', null, new Text(128), 'Bucket name', false)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
|
||||
->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](/docs/environment-variables#storage)', true)
|
||||
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
|
||||
->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true)
|
||||
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
|
||||
->param('compression', COMPRESSION_TYPE_NONE, new WhiteList([COMPRESSION_TYPE_NONE, COMPRESSION_TYPE_GZIP, COMPRESSION_TYPE_ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
|
|
@ -360,10 +364,12 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
->inject('deviceFiles')
|
||||
->inject('deviceLocal')
|
||||
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -496,19 +502,26 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
$metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)];
|
||||
if (!$file->isEmpty()) {
|
||||
$chunks = $file->getAttribute('chunksTotal', 1);
|
||||
$uploaded = $file->getAttribute('chunksUploaded', 0);
|
||||
$metadata = $file->getAttribute('metadata', []);
|
||||
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
|
||||
if ($uploaded === $chunks) {
|
||||
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
$chunksUploaded = $deviceFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
||||
|
||||
if (empty($chunksUploaded)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file');
|
||||
}
|
||||
|
||||
if ($chunksUploaded === $chunks) {
|
||||
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) === Storage::DEVICE_LOCAL) {
|
||||
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceFiles->getType() === Storage::DEVICE_LOCAL) {
|
||||
$antivirus = new Network(
|
||||
App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
|
||||
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
|
||||
|
|
@ -574,6 +587,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
'$id' => $fileId,
|
||||
'$permissions' => $permissions,
|
||||
'bucketId' => $bucket->getId(),
|
||||
'bucketInternalId' => $bucket->getInternalId(),
|
||||
'name' => $fileName,
|
||||
'path' => $path,
|
||||
'signature' => $fileHash,
|
||||
|
|
@ -623,6 +637,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
'$id' => ID::custom($fileId),
|
||||
'$permissions' => $permissions,
|
||||
'bucketId' => $bucket->getId(),
|
||||
'bucketInternalId' => $bucket->getInternalId(),
|
||||
'name' => $fileName,
|
||||
'path' => $path,
|
||||
'signature' => '',
|
||||
|
|
@ -688,10 +703,12 @@ App::get('/v1/storage/buckets/:bucketId/files')
|
|||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(function (string $bucketId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -709,7 +726,9 @@ App::get('/v1/storage/buckets/:bucketId/files')
|
|||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -764,10 +783,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, string $mode) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -836,7 +857,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
|||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -982,7 +1006,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
|
|||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -1119,10 +1146,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
|||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceFiles) {
|
||||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -1270,17 +1299,20 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
->label('sdk.response.model', Response::MODEL_FILE)
|
||||
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
|
||||
->param('fileId', '', new UID(), 'File unique ID.')
|
||||
->param('name', null, new Text(255), 'Name of the file', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permission string. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('mode')
|
||||
->inject('events')
|
||||
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) {
|
||||
|
||||
->action(function (string $bucketId, string $fileId, ?string $name, ?array $permissions, Response $response, Database $dbForProject, Document $user, string $mode, Event $events) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -1332,6 +1364,10 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
|
||||
$file->setAttribute('$permissions', $permissions);
|
||||
|
||||
if (!is_null($name)) {
|
||||
$file->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
if ($fileSecurity && !$valid) {
|
||||
try {
|
||||
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
|
||||
|
|
@ -1352,7 +1388,6 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
});
|
||||
|
||||
App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->alias('/v1/storage/files/:fileId', ['bucketId' => 'default'])
|
||||
->desc('Delete File')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.write')
|
||||
|
|
@ -1381,7 +1416,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, string $mode, Device $deviceFiles, Delete $deletes) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ use Appwrite\Network\Validator\Email;
|
|||
use Utopia\Validator\Host;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Teams;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Limit;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use MaxMind\Db\Reader;
|
||||
|
|
@ -153,7 +153,9 @@ App::get('/v1/teams')
|
|||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -338,19 +340,6 @@ App::delete('/v1/teams/:teamId')
|
|||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$memberships = $dbForProject->find('memberships', [
|
||||
Query::equal('teamId', [$teamId]),
|
||||
Query::limit(2000), // TODO fix members limit
|
||||
]);
|
||||
|
||||
// Memberships are deleted here instead of in the worker to make sure user permisions are updated instantly
|
||||
foreach ($memberships as $membership) {
|
||||
if (!$dbForProject->deleteDocument('memberships', $membership->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB');
|
||||
}
|
||||
$dbForProject->deleteCachedDocument('users', $membership->getAttribute('userId'));
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('teams', $teamId)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
|
||||
}
|
||||
|
|
@ -455,6 +444,14 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
}
|
||||
}
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
try {
|
||||
$userId = ID::unique();
|
||||
$invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
|
||||
|
|
@ -485,7 +482,8 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
'sessions' => null,
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $name])
|
||||
'search' => implode(' ', [$userId, $email, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
])));
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
|
|
@ -545,41 +543,100 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
if (!empty($email)) {
|
||||
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
|
||||
|
||||
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
|
||||
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
|
||||
$body = $locale->getText("emails.invitation.body");
|
||||
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
|
||||
$body->setParam('{{owner}}', $user->getAttribute('name'));
|
||||
$body->setParam('{{team}}', $team->getAttribute('name'));
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.invitation-' . $locale->default] ?? [];
|
||||
|
||||
$body
|
||||
->setParam('{{subject}}', $subject)
|
||||
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
|
||||
->setParam('{{name}}', $user->getAttribute('name'))
|
||||
->setParam('{{body}}', $locale->getText("emails.invitation.body"))
|
||||
->setParam('{{redirect}}', $url)
|
||||
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
|
||||
->setParam('{{signature}}', $locale->getText("emails.invitation.signature"))
|
||||
->setParam('{{project}}', $projectName)
|
||||
->setParam('{{direction}}', $locale->getText('settings.direction'))
|
||||
->setParam('{{bg-body}}', '#f7f7f7')
|
||||
->setParam('{{bg-content}}', '#ffffff')
|
||||
->setParam('{{text-content}}', '#000000');
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message->setParam('{{body}}', $body);
|
||||
$body = $message->render();
|
||||
|
||||
$body = $body->render();
|
||||
$smtp = $project->getAttribute('smtp', []);
|
||||
$smtpEnabled = $smtp['enabled'] ?? false;
|
||||
|
||||
$senderEmail = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
$senderEmail = $smtp['senderEmail'];
|
||||
}
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
}
|
||||
|
||||
$mails
|
||||
->setSmtpHost($smtp['host'] ?? '')
|
||||
->setSmtpPort($smtp['port'] ?? '')
|
||||
->setSmtpUsername($smtp['username'] ?? '')
|
||||
->setSmtpPassword($smtp['password'] ?? '')
|
||||
->setSmtpSecure($smtp['secure'] ?? '');
|
||||
|
||||
if (!empty($customTemplate)) {
|
||||
if (!empty($customTemplate['senderEmail'])) {
|
||||
$senderEmail = $customTemplate['senderEmail'];
|
||||
}
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
$subject = $customTemplate['subject'] ?? $subject;
|
||||
}
|
||||
|
||||
$mails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
|
||||
$emailVariables = [
|
||||
'owner' => $user->getAttribute('name'),
|
||||
'team' => $team->getAttribute('name'),
|
||||
'subject' => $subject,
|
||||
'hello' => $locale->getText("emails.invitation.hello"),
|
||||
'name' => $user->getAttribute('name'),
|
||||
'body' => $body,
|
||||
'redirect' => $url,
|
||||
'footer' => $locale->getText("emails.invitation.footer"),
|
||||
'thanks' => $locale->getText("emails.invitation.thanks"),
|
||||
'signature' => $locale->getText("emails.invitation.signature"),
|
||||
'project' => $projectName,
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
'bg-body' => '#f7f7f7',
|
||||
'bg-content' => '#ffffff',
|
||||
'text-content' => '#000000',
|
||||
];
|
||||
|
||||
$mails
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setFrom($from)
|
||||
->setRecipient($invitee->getAttribute('email'))
|
||||
->setName($invitee->getAttribute('name'))
|
||||
->setVariables($emailVariables)
|
||||
->trigger()
|
||||
;
|
||||
} elseif (!empty($phone)) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'];
|
||||
}
|
||||
|
||||
$message = $message->setParam('{{token}}', $url);
|
||||
$message = $message->render();
|
||||
|
||||
$messaging
|
||||
->setRecipient($phone)
|
||||
->setMessage($url)
|
||||
->setMessage($message)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
|
@ -635,7 +692,9 @@ App::get('/v1/teams/:teamId/memberships')
|
|||
$queries[] = Query::equal('teamId', [$teamId]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -725,7 +784,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
|
|||
});
|
||||
|
||||
App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
->desc('Update Membership Roles')
|
||||
->desc('Update Membership')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.[teamId].memberships.[membershipId].update')
|
||||
->label('scope', 'teams.write')
|
||||
|
|
@ -733,8 +792,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
|||
->label('audits.resource', 'team/{request.teamId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'teams')
|
||||
->label('sdk.method', 'updateMembershipRoles')
|
||||
->label('sdk.description', '/docs/references/teams/update-team-membership-roles.md')
|
||||
->label('sdk.method', 'updateMembership')
|
||||
->label('sdk.description', '/docs/references/teams/update-team-membership.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
|
||||
|
|
@ -849,7 +908,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
|||
}
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
$user = $dbForProject->getDocument('users', $userId); // Get user
|
||||
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
|
||||
}
|
||||
|
||||
if ($membership->getAttribute('userId') !== $user->getId()) {
|
||||
|
|
@ -865,7 +924,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
|||
->setAttribute('confirm', true)
|
||||
;
|
||||
|
||||
$user = Authorization::skip(fn() => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
|
||||
Authorization::skip(fn() => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
|
||||
|
||||
// Log user in
|
||||
|
||||
|
|
@ -1008,7 +1067,7 @@ App::get('/v1/teams/:teamId/logs')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_LOG_LIST)
|
||||
->param('teamId', '', new UID(), 'Team ID.')
|
||||
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ use Appwrite\Event\Delete;
|
|||
use Appwrite\Event\Event;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Users;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Limit;
|
||||
use Appwrite\Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
|
|
@ -28,6 +29,7 @@ use Utopia\Database\Validator\UID;
|
|||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Validator\Text;
|
||||
|
|
@ -36,6 +38,7 @@ use MaxMind\Db\Reader;
|
|||
use Utopia\Validator\Integer;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $events): Document
|
||||
|
|
@ -45,6 +48,14 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
|||
|
||||
if (!empty($email)) {
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -52,6 +63,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
|||
? ID::unique()
|
||||
: ID::custom($userId);
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, $phone);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||
$user = $dbForProject->createDocument('users', new Document([
|
||||
'$id' => $userId,
|
||||
|
|
@ -65,6 +83,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
|||
'phone' => $phone,
|
||||
'phoneVerification' => false,
|
||||
'status' => true,
|
||||
'labels' => [],
|
||||
'password' => $password,
|
||||
'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password],
|
||||
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
|
||||
|
|
@ -77,7 +96,8 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
|||
'sessions' => null,
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name])
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
'accessedAt' => DateTime::now(),
|
||||
]));
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
|
|
@ -386,7 +406,9 @@ App::get('/v1/users')
|
|||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
|
@ -557,7 +579,7 @@ App::get('/v1/users/:userId/logs')
|
|||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_LOG_LIST)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
|
|
@ -626,6 +648,55 @@ App::get('/v1/users/:userId/logs')
|
|||
]), Response::MODEL_LOG_LIST);
|
||||
});
|
||||
|
||||
App::get('/v1/users/identities')
|
||||
->desc('List Identities')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'listIdentities')
|
||||
->label('sdk.description', '/docs/references/users/list-identities.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_IDENTITY_LIST)
|
||||
->param('queries', [], new Identities(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Identities::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
$identityId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "User '{$identityId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'identities' => $dbForProject->find('identities', $queries),
|
||||
'total' => $dbForProject->count('identities', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_IDENTITY_LIST);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/status')
|
||||
->desc('Update User Status')
|
||||
->groups(['api', 'users'])
|
||||
|
|
@ -663,27 +734,27 @@ App::patch('/v1/users/:userId/status')
|
|||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/verification')
|
||||
->desc('Update Email Verification')
|
||||
App::put('/v1/users/:userId/labels')
|
||||
->desc('Update User Labels')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.verification')
|
||||
->label('event', 'users.[userId].update.labels')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'verification.update')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateEmailVerification')
|
||||
->label('sdk.description', '/docs/references/users/update-user-email-verification.md')
|
||||
->label('sdk.method', 'updateLabels')
|
||||
->label('sdk.description', '/docs/references/users/update-user-labels.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('emailVerification', false, new Boolean(), 'User email verification status.')
|
||||
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), 5), 'Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Event $events) {
|
||||
->action(function (string $userId, array $labels, Response $response, Database $dbForProject, Event $events) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
|
|
@ -691,7 +762,9 @@ App::patch('/v1/users/:userId/verification')
|
|||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
|
||||
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId());
|
||||
|
|
@ -764,10 +837,7 @@ App::patch('/v1/users/:userId/name')
|
|||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email', ''), $name, $user->getAttribute('phone', '')]));
|
||||
;
|
||||
$user->setAttribute('name', $name);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
|
|
@ -806,19 +876,25 @@ App::patch('/v1/users/:userId/password')
|
|||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $user->getAttribute('email'), $user->getAttribute('name'), $user->getAttribute('phone'));
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = [];
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
if ($historyLimit > 0) {
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED, 'The password was recently used', 409);
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
|
||||
$history[] = $newPassword;
|
||||
array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
$user
|
||||
|
|
@ -866,10 +942,20 @@ App::patch('/v1/users/:userId/email')
|
|||
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name', ''), $user->getAttribute('phone', '')]));
|
||||
;
|
||||
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
|
@ -913,7 +999,6 @@ App::patch('/v1/users/:userId/phone')
|
|||
$user
|
||||
->setAttribute('phone', $number)
|
||||
->setAttribute('phoneVerification', false)
|
||||
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $number]));
|
||||
;
|
||||
|
||||
try {
|
||||
|
|
@ -1127,6 +1212,38 @@ App::delete('/v1/users/:userId')
|
|||
$response->noContent();
|
||||
});
|
||||
|
||||
App::delete('/v1/users/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'identity.delete')
|
||||
->label('audits.resource', 'identity/{request.$identityId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'deleteIdentity')
|
||||
->label('sdk.description', '/docs/references/users/delete-identity.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
->inject('deletes')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
if ($identity->isEmpty()) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/users/usage')
|
||||
->desc('Get usage stats for the users API')
|
||||
->groups(['api', 'users', 'usage'])
|
||||
|
|
|
|||
1080
app/controllers/api/vcs.php
Normal file
1080
app/controllers/api/vcs.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,9 @@ use Utopia\Locale\Locale;
|
|||
use Utopia\Logger\Logger;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Pools\Group;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\View;
|
||||
|
|
@ -22,6 +25,7 @@ use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
|
|||
use Appwrite\Utopia\Response\Filters\V13 as ResponseV13;
|
||||
use Appwrite\Utopia\Response\Filters\V14 as ResponseV14;
|
||||
use Appwrite\Utopia\Response\Filters\V15 as ResponseV15;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
|
|
@ -33,6 +37,7 @@ use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
|
|||
use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
|
||||
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
|
||||
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
|
|
@ -40,9 +45,136 @@ Config::setParam('domainVerification', false);
|
|||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response)
|
||||
{
|
||||
$utopia->getRoute()->label('error', __DIR__ . '/../views/general/error.phtml');
|
||||
|
||||
$host = $request->getHostname() ?? '';
|
||||
|
||||
$route = Authorization::skip(
|
||||
fn () => $dbForConsole->find('rules', [
|
||||
Query::equal('domain', [$host]),
|
||||
Query::limit(1)
|
||||
])
|
||||
)[0] ?? null;
|
||||
|
||||
if ($route === null) {
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
|
||||
if ($mainDomain === 'localhost') {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_DOMAIN_NOT_CONFIGURED);
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::ROUTER_HOST_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
$projectId = $route->getAttribute('projectId');
|
||||
$project = Authorization::skip(
|
||||
fn () => $dbForConsole->getDocument('projects', $projectId)
|
||||
);
|
||||
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
|
||||
$status = $project->getAttribute('services', [])['proxy'];
|
||||
if (!$status) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip Appwrite Router for ACME challenge. Nessessary for certificate generation
|
||||
$path = ($swooleRequest->server['request_uri'] ?? '/');
|
||||
if (\str_starts_with($path, '/.well-known/acme-challenge')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $route->getAttribute('resourceType');
|
||||
|
||||
if ($type === 'function') {
|
||||
$functionId = $route->getAttribute('resourceId');
|
||||
$projectId = $route->getAttribute('projectId');
|
||||
|
||||
$path = ($swooleRequest->server['request_uri'] ?? '/');
|
||||
$query = ($swooleRequest->server['query_string'] ?? '');
|
||||
if (!empty($query)) {
|
||||
$path .= '?' . $query;
|
||||
}
|
||||
|
||||
$body = \json_encode([
|
||||
'async' => false,
|
||||
'body' => $swooleRequest->getContent() ?? '',
|
||||
'method' => $swooleRequest->server['request_method'],
|
||||
'path' => $path,
|
||||
'headers' => $swooleRequest->header
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'X-Appwrite-Project: ' . $projectId
|
||||
];
|
||||
|
||||
$ch = \curl_init();
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// \curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
|
||||
$executionResponse = \curl_exec($ch);
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = \curl_error($ch);
|
||||
$errNo = \curl_errno($ch);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$error = \json_decode($executionResponse, true)['message'];
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error);
|
||||
}
|
||||
|
||||
$execution = \json_decode($executionResponse, true);
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($execution['responseHeaders'] as $header) {
|
||||
if (\strtolower($header['name']) === 'content-type') {
|
||||
$contentType = $header['value'];
|
||||
}
|
||||
|
||||
$response->setHeader($header['name'], $header['value']);
|
||||
}
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
|
||||
$encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name'));
|
||||
if ($encodingKey !== false) {
|
||||
if (($execution['responseHeaders'][$encodingKey]['value'] ?? '') === 'base64') {
|
||||
$body = \base64_decode($body);
|
||||
}
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType($contentType)
|
||||
->setStatusCode($execution['responseStatusCode'] ?? 200)
|
||||
->send($body);
|
||||
|
||||
return true;
|
||||
} elseif ($type === 'api') {
|
||||
return false;
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
App::init()
|
||||
->groups(['api'])
|
||||
->groups(['api', 'web'])
|
||||
->inject('utopia')
|
||||
->inject('swooleRequest')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('console')
|
||||
|
|
@ -50,15 +182,33 @@ App::init()
|
|||
->inject('dbForConsole')
|
||||
->inject('user')
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('clients')
|
||||
->inject('servers')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers) {
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Request format
|
||||
*/
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
Request::setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
return $response->setStatusCode(404)->send('Not Found');
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($requestFormat) {
|
||||
switch ($requestFormat) {
|
||||
|
|
@ -74,6 +224,9 @@ App::init()
|
|||
case version_compare($requestFormat, '0.15.3', '<'):
|
||||
Request::setFilter(new RequestV15());
|
||||
break;
|
||||
case version_compare($requestFormat, '1.4.0', '<'):
|
||||
Request::setFilter(new RequestV16());
|
||||
break;
|
||||
default:
|
||||
Request::setFilter(null);
|
||||
}
|
||||
|
|
@ -81,62 +234,8 @@ App::init()
|
|||
Request::setFilter(null);
|
||||
}
|
||||
|
||||
$domain = $request->getHostname();
|
||||
$domains = Config::getParam('domains', []);
|
||||
if (!array_key_exists($domain, $domains)) {
|
||||
$domain = new Domain(!empty($domain) ? $domain : '');
|
||||
|
||||
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
|
||||
$domains[$domain->get()] = false;
|
||||
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
|
||||
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
|
||||
Console::warning('Skipping SSL certificates generation on ACME challenge.');
|
||||
} else {
|
||||
Authorization::disable();
|
||||
|
||||
$envDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
$mainDomain = null;
|
||||
if (!empty($envDomain) && $envDomain !== 'localhost') {
|
||||
$mainDomain = $envDomain;
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [Query::orderAsc('_id')]);
|
||||
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
|
||||
}
|
||||
|
||||
if ($mainDomain !== $domain->get()) {
|
||||
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
|
||||
} else {
|
||||
$domainDocument = $dbForConsole->findOne('domains', [
|
||||
Query::equal('domain', [$domain->get()])
|
||||
]);
|
||||
|
||||
if (!$domainDocument) {
|
||||
$domainDocument = new Document([
|
||||
'domain' => $domain->get(),
|
||||
'tld' => $domain->getSuffix(),
|
||||
'registerable' => $domain->getRegisterable(),
|
||||
'verification' => false,
|
||||
'certificateId' => null,
|
||||
]);
|
||||
|
||||
$domainDocument = $dbForConsole->createDocument('domains', $domainDocument);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
|
||||
(new Certificate())
|
||||
->setDomain($domainDocument)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
$domains[$domain->get()] = true;
|
||||
|
||||
Authorization::reset(); // ensure authorization is re-enabled
|
||||
}
|
||||
Config::setParam('domains', $domains);
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
if (\in_array($localeParam, Config::getParam('locale-codes'))) {
|
||||
if (\in_array($localeParam, $localeCodes)) {
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
|
|
@ -171,16 +270,24 @@ App::init()
|
|||
Config::setParam(
|
||||
'domainVerification',
|
||||
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
|
||||
$endDomain->getRegisterable() !== ''
|
||||
$endDomain->getRegisterable() !== ''
|
||||
);
|
||||
|
||||
Config::setParam('cookieDomain', (
|
||||
$request->getHostname() === 'localhost' ||
|
||||
$request->getHostname() === 'localhost:' . $request->getPort() ||
|
||||
(\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false)
|
||||
)
|
||||
? null
|
||||
: '.' . $request->getHostname());
|
||||
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
|
||||
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
|
||||
|
||||
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
|
||||
$isConsoleRootSession = App::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
|
||||
|
||||
Config::setParam(
|
||||
'cookieDomain',
|
||||
$isLocalHost || $isIpAddress
|
||||
? null
|
||||
: ($isConsoleProject && $isConsoleRootSession
|
||||
? '.' . $selfDomain->getRegisterable()
|
||||
: '.' . $request->getHostname()
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Response format
|
||||
|
|
@ -203,6 +310,9 @@ App::init()
|
|||
case version_compare($responseFormat, '0.15.3', '<='):
|
||||
Response::setFilter(new ResponseV15());
|
||||
break;
|
||||
case version_compare($responseFormat, '1.4.0', '<'):
|
||||
Response::setFilter(new ResponseV16());
|
||||
break;
|
||||
default:
|
||||
Response::setFilter(null);
|
||||
}
|
||||
|
|
@ -217,7 +327,7 @@ App::init()
|
|||
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
||||
*/
|
||||
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https') {
|
||||
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // Localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP.');
|
||||
}
|
||||
|
|
@ -237,8 +347,7 @@ App::init()
|
|||
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
|
||||
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
|
||||
->addHeader('Access-Control-Allow-Origin', $refDomain)
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true')
|
||||
;
|
||||
->addHeader('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack
|
||||
|
|
@ -265,7 +374,7 @@ App::init()
|
|||
: Role::users()->toString();
|
||||
|
||||
// Add user roles
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships');
|
||||
|
||||
if ($memberships) {
|
||||
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
|
||||
|
|
@ -311,7 +420,7 @@ App::init()
|
|||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
throw new AppwriteException(AppwriteException:: PROJECT_KEY_EXPIRED);
|
||||
throw new AppwriteException(AppwriteException::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
|
|
@ -376,9 +485,23 @@ App::init()
|
|||
});
|
||||
|
||||
App::options()
|
||||
->inject('utopia')
|
||||
->inject('swooleRequest')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (Request $request, Response $response) {
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$origin = $request->getOrigin();
|
||||
|
||||
|
|
@ -403,7 +526,7 @@ App::error()
|
|||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if ($logger) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
|
|
@ -426,6 +549,7 @@ App::error()
|
|||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('database', $project->getAttribute('database', 'console'));
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
|
|
@ -538,8 +662,7 @@ App::error()
|
|||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code)
|
||||
;
|
||||
->setStatusCode($code);
|
||||
|
||||
$template = ($route) ? $route->getLabel('error', null) : null;
|
||||
|
||||
|
|
@ -552,9 +675,9 @@ App::error()
|
|||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('type', $type)
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace)
|
||||
;
|
||||
->setParam('trace', $trace);
|
||||
|
||||
$response->html($layout->render());
|
||||
}
|
||||
|
|
@ -636,6 +759,14 @@ App::get('/.well-known/acme-challenge/*')
|
|||
});
|
||||
|
||||
include_once __DIR__ . '/shared/api.php';
|
||||
include_once __DIR__ . '/shared/api/auth.php';
|
||||
|
||||
App::wildcard()
|
||||
->groups(['api'])
|
||||
->label('scope', 'global')
|
||||
->action(function () {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
});
|
||||
|
||||
foreach (Config::getParam('services', []) as $service) {
|
||||
include_once $service['controller'];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use Utopia\Database\Document;
|
|||
use Utopia\Validator\Host;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\App;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Integer;
|
||||
|
|
@ -199,35 +198,6 @@ App::delete('/v1/mock/tests/bar')
|
|||
->action(function ($required, $default, $z) {
|
||||
});
|
||||
|
||||
/** Endpoint to test if required headers are sent from the SDK */
|
||||
App::get('/v1/mock/tests/general/headers')
|
||||
->desc('Get headers')
|
||||
->groups(['mock'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'general')
|
||||
->label('sdk.method', 'headers')
|
||||
->label('sdk.description', 'Return headers from the request')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.model', Response::MODEL_MOCK)
|
||||
->label('sdk.mock', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (Request $request, Response $response) {
|
||||
$res = [
|
||||
'x-sdk-name' => $request->getHeader('x-sdk-name'),
|
||||
'x-sdk-platform' => $request->getHeader('x-sdk-platform'),
|
||||
'x-sdk-language' => $request->getHeader('x-sdk-language'),
|
||||
'x-sdk-version' => $request->getHeader('x-sdk-version'),
|
||||
];
|
||||
$res = array_map(function ($key, $value) {
|
||||
return $key . ': ' . $value;
|
||||
}, array_keys($res), $res);
|
||||
$res = implode("; ", $res);
|
||||
|
||||
$response->dynamic(new Document(['result' => $res]), Response::MODEL_MOCK);
|
||||
});
|
||||
|
||||
App::get('/v1/mock/tests/general/download')
|
||||
->desc('Download File')
|
||||
->groups(['mock'])
|
||||
|
|
@ -444,35 +414,6 @@ App::post('/v1/mock/tests/general/nullable')
|
|||
->action(function (string $required, string $nullable, ?string $optional) {
|
||||
});
|
||||
|
||||
/** Endpoint to test if required headers are sent from the SDK */
|
||||
App::get('/v1/mock/tests/general/headers')
|
||||
->desc('Get headers')
|
||||
->groups(['mock'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'general')
|
||||
->label('sdk.method', 'headers')
|
||||
->label('sdk.description', 'Return headers from the request')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.model', Response::MODEL_MOCK)
|
||||
->label('sdk.mock', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (Request $request, Response $response) {
|
||||
$res = [
|
||||
'x-sdk-name' => $request->getHeader('x-sdk-name'),
|
||||
'x-sdk-platform' => $request->getHeader('x-sdk-platform'),
|
||||
'x-sdk-language' => $request->getHeader('x-sdk-language'),
|
||||
'x-sdk-version' => $request->getHeader('x-sdk-version'),
|
||||
];
|
||||
$res = array_map(function ($key, $value) {
|
||||
return $key . ': ' . $value;
|
||||
}, array_keys($res), $res);
|
||||
$res = implode("; ", $res);
|
||||
|
||||
$response->dynamic(new Document(['result' => $res]), Response::MODEL_MOCK);
|
||||
});
|
||||
|
||||
App::get('/v1/mock/tests/general/400-error')
|
||||
->desc('400 Error')
|
||||
->groups(['mock'])
|
||||
|
|
@ -644,7 +585,7 @@ App::shutdown()
|
|||
->action(function (App $utopia, Response $response, Request $request) {
|
||||
|
||||
$result = [];
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
$path = APP_STORAGE_CACHE . '/tests.json';
|
||||
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use Appwrite\Event\Audit;
|
|||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Usage\Stats;
|
||||
|
|
@ -96,14 +98,15 @@ App::init()
|
|||
->inject('user')
|
||||
->inject('events')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('deletes')
|
||||
->inject('database')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) use ($databaseListener) {
|
||||
->inject('mails')
|
||||
->inject('usage')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode, Mail $mails, Stats $usage) use ($databaseListener) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope
|
||||
throw new Exception(Exception::PROJECT_UNKNOWN);
|
||||
|
|
@ -120,11 +123,11 @@ App::init()
|
|||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod());
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod());
|
||||
$timeLimitArray[] = $timeLimit;
|
||||
}
|
||||
|
||||
|
|
@ -167,9 +170,9 @@ App::init()
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Background Jobs
|
||||
*/
|
||||
/*
|
||||
* Background Jobs
|
||||
*/
|
||||
$events
|
||||
->setEvent($route->getLabel('event', ''))
|
||||
->setProject($project)
|
||||
|
|
@ -194,9 +197,8 @@ App::init()
|
|||
$deletes->setProject($project);
|
||||
$database->setProject($project);
|
||||
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_CREATE, fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_DELETE, fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
$dbForProject->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, Document $document) => $databaseListener($event, $document, $usage));
|
||||
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
|
||||
|
|
@ -218,7 +220,10 @@ App::init()
|
|||
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +269,7 @@ App::init()
|
|||
->inject('project')
|
||||
->action(function (App $utopia, Request $request, Document $project) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
|
|
@ -356,14 +361,17 @@ App::shutdown()
|
|||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('events')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('deletes')
|
||||
->inject('database')
|
||||
->inject('mode')
|
||||
->inject('dbForProject')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) {
|
||||
->inject('queueForFunctions')
|
||||
->inject('mode')
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
|
|
@ -371,12 +379,12 @@ App::shutdown()
|
|||
if (empty($events->getPayload())) {
|
||||
$events->setPayload($responsePayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger functions.
|
||||
*/
|
||||
$events
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
$queueForFunctions
|
||||
->from($events)
|
||||
->trigger();
|
||||
|
||||
/**
|
||||
|
|
@ -422,9 +430,8 @@ App::shutdown()
|
|||
}
|
||||
}
|
||||
|
||||
$route = $utopia->match($request);
|
||||
$route = $utopia->getRoute();
|
||||
$requestParams = $route->getParamsValues();
|
||||
$user = $audits->getUser();
|
||||
|
||||
/**
|
||||
* Audit labels
|
||||
|
|
@ -437,10 +444,7 @@ App::shutdown()
|
|||
}
|
||||
}
|
||||
|
||||
$pattern = $route->getLabel('audits.userId', null);
|
||||
if (!empty($pattern)) {
|
||||
$userId = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
if (!$user->isEmpty()) {
|
||||
$audits->setUser($user);
|
||||
}
|
||||
|
||||
|
|
@ -489,14 +493,14 @@ App::shutdown()
|
|||
|
||||
$key = md5($request->getURI() . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER;
|
||||
$data = json_encode([
|
||||
'resourceType' => $resourceType,
|
||||
'resource' => $resource,
|
||||
'contentType' => $response->getContentType(),
|
||||
'payload' => base64_encode($data['payload']),
|
||||
'resourceType' => $resourceType,
|
||||
'resource' => $resource,
|
||||
'contentType' => $response->getContentType(),
|
||||
'payload' => base64_encode($data['payload']),
|
||||
]) ;
|
||||
|
||||
$signature = md5($data);
|
||||
$cacheLog = $dbForProject->getDocument('cache', $key);
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
|
||||
$now = DateTime::now();
|
||||
if ($cacheLog->isEmpty()) {
|
||||
|
|
@ -552,11 +556,3 @@ App::shutdown()
|
|||
->submit();
|
||||
}
|
||||
});
|
||||
|
||||
App::init()
|
||||
->groups(['usage'])
|
||||
->action(function () {
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') {
|
||||
throw new Exception(Exception::GENERAL_USAGE_DISABLED);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
62
app/controllers/shared/api/auth.php
Normal file
62
app/controllers/shared/api/auth.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\App;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
||||
App::init()
|
||||
->groups(['auth'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->action(function (App $utopia, Request $request, Document $project) {
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
switch ($route->getLabel('auth.type', '')) {
|
||||
case 'emailPassword':
|
||||
if (($auths['emailPassword'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'anonymous':
|
||||
if (($auths['anonymous'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Anonymous authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jwt':
|
||||
if (($auths['JWT'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'JWT authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
|
|
@ -21,13 +22,61 @@ App::get('/console/*')
|
|||
->alias('auth/*')
|
||||
->alias('/invite')
|
||||
->alias('/login')
|
||||
->alias('/card/*')
|
||||
->alias('/recover')
|
||||
->alias('/register/*')
|
||||
->groups(['web'])
|
||||
->label('permission', 'public')
|
||||
->label('scope', 'home')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
->action(function (Request $request, Response $response) {
|
||||
// Serve static files (console) only for main domain
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) {
|
||||
throw new Exception(Exception::GENERAL_ROUTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$fallback = file_get_contents(__DIR__ . '/../../../console/index.html');
|
||||
|
||||
// Card SSR
|
||||
if (\str_starts_with($request->getURI(), '/card')) {
|
||||
$urlCunks = \explode('/', $request->getURI());
|
||||
$userId = $urlCunks[\count($urlCunks) - 1] ?? '';
|
||||
|
||||
$domain = $request->getProtocol() . '://' . $request->getHostname();
|
||||
|
||||
if (!empty($userId)) {
|
||||
$ogImageUrl = $domain . '/v1/cards/cloud-og?userId=' . $userId;
|
||||
} else {
|
||||
$ogImageUrl = $domain . '/v1/cards/cloud-og?mock=normal';
|
||||
}
|
||||
|
||||
$ogTags = [
|
||||
'<title>Appwrite Cloud Card</title>',
|
||||
'<meta name="description" content="Appwrite Cloud is now LIVE! Share your Cloud card for a chance to win an exclusive Cloud hoodie!">',
|
||||
'<meta property="og:url" content="' . $domain . $request->getURI() . '">',
|
||||
'<meta name="og:image:type" content="image/png">',
|
||||
'<meta name="og:image:width" content="1008">',
|
||||
'<meta name="og:image:height" content="1008">',
|
||||
'<meta property="og:type" content="website">',
|
||||
'<meta property="og:title" content="Appwrite Cloud Card">',
|
||||
'<meta property="og:description" content="Appwrite Cloud is now LIVE! Share your Cloud card for a chance to win an exclusive Cloud hoodie!">',
|
||||
'<meta property="og:image" content="' . $ogImageUrl . '">',
|
||||
'<meta name="twitter:card" content="summary_large_image">',
|
||||
'<meta property="twitter:domain" content="' . $request->getHostname() . '">',
|
||||
'<meta property="twitter:url" content="' . $domain . $request->getURI() . '">',
|
||||
'<meta name="twitter:title" content="Appwrite Cloud Card">',
|
||||
'<meta name="twitter:image:type" content="image/png">',
|
||||
'<meta name="twitter:image:width" content="1008">',
|
||||
'<meta name="twitter:image:height" content="1008">',
|
||||
'<meta name="twitter:description" content="Appwrite Cloud is now LIVE! Share your Cloud card for a chance to win an exclusive Cloud hoodie!">',
|
||||
'<meta name="twitter:image" content="' . $ogImageUrl . '">',
|
||||
];
|
||||
|
||||
$fallback = \str_replace('<!-- {{CLOUD_OG}} -->', \implode('', $ogTags), $fallback);
|
||||
}
|
||||
|
||||
$response->html($fallback);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use Utopia\Config\Config;
|
|||
|
||||
App::get('/versions')
|
||||
->desc('Get Version')
|
||||
->groups(['home'])
|
||||
->groups(['home', 'web'])
|
||||
->label('scope', 'public')
|
||||
->inject('response')
|
||||
->action(function (Response $response) {
|
||||
|
|
|
|||
806
app/executor.php
806
app/executor.php
|
|
@ -1,806 +0,0 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Swoole\ConnectionPool;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Swoole\Http\Server;
|
||||
use Swoole\Process;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Timer;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Orchestration\Adapter\DockerCLI;
|
||||
use Utopia\Orchestration\Orchestration;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Device\Backblaze;
|
||||
use Utopia\Storage\Device\DOSpaces;
|
||||
use Utopia\Storage\Device\Linode;
|
||||
use Utopia\Storage\Device\Wasabi;
|
||||
use Utopia\Storage\Device\S3;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\Swoole\Response;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
|
||||
Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
|
||||
|
||||
/** Constants */
|
||||
const MAINTENANCE_INTERVAL = 3600; // 3600 seconds = 1 hour
|
||||
|
||||
/**
|
||||
* Create a Swoole table to store runtime information
|
||||
*/
|
||||
$activeRuntimes = new Swoole\Table(1024);
|
||||
$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 256);
|
||||
$activeRuntimes->column('created', Swoole\Table::TYPE_INT, 8);
|
||||
$activeRuntimes->column('updated', Swoole\Table::TYPE_INT, 8);
|
||||
$activeRuntimes->column('name', Swoole\Table::TYPE_STRING, 128);
|
||||
$activeRuntimes->column('status', Swoole\Table::TYPE_STRING, 128);
|
||||
$activeRuntimes->column('key', Swoole\Table::TYPE_STRING, 256);
|
||||
$activeRuntimes->create();
|
||||
|
||||
/**
|
||||
* Create orchestration pool
|
||||
*/
|
||||
$orchestrationPool = new ConnectionPool(function () {
|
||||
$dockerUser = App::getEnv('DOCKERHUB_PULL_USERNAME', null);
|
||||
$dockerPass = App::getEnv('DOCKERHUB_PULL_PASSWORD', null);
|
||||
$orchestration = new Orchestration(new DockerCLI($dockerUser, $dockerPass));
|
||||
return $orchestration;
|
||||
}, 10);
|
||||
|
||||
|
||||
/**
|
||||
* Create logger instance
|
||||
*/
|
||||
$providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
|
||||
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
$logger = null;
|
||||
|
||||
if (!empty($providerName) && !empty($providerConfig) && Logger::hasProvider($providerName)) {
|
||||
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName);
|
||||
$adapter = new $classname($providerConfig);
|
||||
$logger = new Logger($adapter);
|
||||
}
|
||||
|
||||
function logError(Throwable $error, string $action, Utopia\Route $route = null)
|
||||
{
|
||||
global $logger;
|
||||
|
||||
if ($logger) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace("executor");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
if ($route) {
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
}
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Executor log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
}
|
||||
|
||||
function getStorageDevice($root): Device
|
||||
{
|
||||
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
case Storage::DEVICE_S3:
|
||||
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
|
||||
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
|
||||
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
|
||||
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
|
||||
$s3Acl = 'private';
|
||||
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
|
||||
case Storage::DEVICE_DO_SPACES:
|
||||
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
|
||||
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
|
||||
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
|
||||
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
|
||||
$doSpacesAcl = 'private';
|
||||
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
|
||||
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
|
||||
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
|
||||
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
|
||||
$backblazeAcl = 'private';
|
||||
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
|
||||
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
|
||||
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
|
||||
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
|
||||
$linodeAcl = 'private';
|
||||
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
|
||||
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
|
||||
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
|
||||
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
|
||||
$wasabiAcl = 'private';
|
||||
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
|
||||
}
|
||||
}
|
||||
|
||||
App::post('/v1/runtimes')
|
||||
->desc("Create a new runtime server")
|
||||
->param('runtimeId', '', new Text(64), 'Unique runtime ID.')
|
||||
->param('source', '', new Text(0), 'Path to source files.')
|
||||
->param('destination', '', new Text(0, 0), 'Destination folder to store build files into.', true)
|
||||
->param('vars', [], new Assoc(), 'Environment Variables required for the build.')
|
||||
->param('commands', [], new ArrayList(new Text(1024), 100), 'Commands required to build the container. Maximum of 100 commands are allowed, each 1024 characters long.')
|
||||
->param('runtime', '', new Text(128), 'Runtime for the cloud function.')
|
||||
->param('baseImage', '', new Text(128), 'Base image name of the runtime.')
|
||||
->param('entrypoint', '', new Text(256), 'Entrypoint of the code file.', true)
|
||||
->param('remove', false, new Boolean(), 'Remove a runtime after execution.')
|
||||
->param('workdir', '', new Text(256, 0), 'Working directory.', true)
|
||||
->inject('orchestrationPool')
|
||||
->inject('activeRuntimes')
|
||||
->inject('response')
|
||||
->action(function (string $runtimeId, string $source, string $destination, array $vars, array $commands, string $runtime, string $baseImage, string $entrypoint, bool $remove, string $workdir, $orchestrationPool, $activeRuntimes, Response $response) {
|
||||
if ($activeRuntimes->exists($runtimeId)) {
|
||||
if ($activeRuntimes->get($runtimeId)['status'] == 'pending') {
|
||||
throw new \Exception('A runtime with the same ID is already being created. Attempt a execution soon.', 500);
|
||||
}
|
||||
|
||||
throw new Exception('Runtime already exists.', 409);
|
||||
}
|
||||
|
||||
$container = [];
|
||||
$containerId = '';
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$startTime = DateTime::now();
|
||||
$startTimeUnix = (new \DateTime($startTime))->getTimestamp();
|
||||
$endTimeUnix = 0;
|
||||
$orchestration = $orchestrationPool->get();
|
||||
|
||||
$secret = \bin2hex(\random_bytes(16));
|
||||
|
||||
if (!$remove) {
|
||||
$activeRuntimes->set($runtimeId, [
|
||||
'id' => $containerId,
|
||||
'name' => $runtimeId,
|
||||
'created' => $startTimeUnix,
|
||||
'updated' => $endTimeUnix,
|
||||
'status' => 'pending',
|
||||
'key' => $secret,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
Console::info('Building container : ' . $runtimeId);
|
||||
|
||||
/**
|
||||
* Temporary file paths in the executor
|
||||
*/
|
||||
$tmpSource = "/tmp/$runtimeId/src/code.tar.gz";
|
||||
$tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz";
|
||||
|
||||
/**
|
||||
* Copy code files from source to a temporary location on the executor
|
||||
*/
|
||||
$sourceDevice = getStorageDevice("/");
|
||||
$localDevice = new Local();
|
||||
$buffer = $sourceDevice->read($source);
|
||||
if (!$localDevice->write($tmpSource, $buffer)) {
|
||||
throw new Exception('Failed to copy source code to temporary directory', 500);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the mount folder
|
||||
*/
|
||||
if (!\file_exists(\dirname($tmpBuild))) {
|
||||
if (!@\mkdir(\dirname($tmpBuild), 0755, true)) {
|
||||
throw new Exception("Failed to create temporary directory", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create container
|
||||
*/
|
||||
$vars = \array_merge($vars, [
|
||||
'INTERNAL_RUNTIME_KEY' => $secret,
|
||||
'INTERNAL_RUNTIME_ENTRYPOINT' => $entrypoint,
|
||||
]);
|
||||
$vars = array_map(fn ($v) => strval($v), $vars);
|
||||
$orchestration
|
||||
->setCpus((int) App::getEnv('_APP_FUNCTIONS_CPUS', 0))
|
||||
->setMemory((int) App::getEnv('_APP_FUNCTIONS_MEMORY', 0))
|
||||
->setSwap((int) App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 0));
|
||||
|
||||
/** Keep the container alive if we have commands to be executed */
|
||||
$entrypoint = !empty($commands) ? [
|
||||
'tail',
|
||||
'-f',
|
||||
'/dev/null'
|
||||
] : [];
|
||||
|
||||
$containerId = $orchestration->run(
|
||||
image: $baseImage,
|
||||
name: $runtimeId,
|
||||
hostname: $runtimeId,
|
||||
vars: $vars,
|
||||
command: $entrypoint,
|
||||
labels: [
|
||||
'openruntimes-id' => $runtimeId,
|
||||
'openruntimes-type' => 'runtime',
|
||||
'openruntimes-created' => strval($startTimeUnix),
|
||||
'openruntimes-runtime' => $runtime,
|
||||
],
|
||||
workdir: $workdir,
|
||||
volumes: [
|
||||
\dirname($tmpSource) . ':/tmp:rw',
|
||||
\dirname($tmpBuild) . ':/usr/code:rw'
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($containerId)) {
|
||||
throw new Exception('Failed to create build container', 500);
|
||||
}
|
||||
|
||||
$orchestration->networkConnect($runtimeId, App::getEnv('OPEN_RUNTIMES_NETWORK', 'appwrite_runtimes'));
|
||||
|
||||
/**
|
||||
* Execute any commands if they were provided
|
||||
*/
|
||||
if (!empty($commands)) {
|
||||
$status = $orchestration->execute(
|
||||
name: $runtimeId,
|
||||
command: $commands,
|
||||
stdout: $stdout,
|
||||
stderr: $stderr,
|
||||
timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900)
|
||||
);
|
||||
|
||||
if (!$status) {
|
||||
throw new Exception('Failed to build dependenices ' . $stderr, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move built code to expected build directory
|
||||
*/
|
||||
if (!empty($destination)) {
|
||||
// Check if the build was successful by checking if file exists
|
||||
if (!\file_exists($tmpBuild)) {
|
||||
throw new Exception('Something went wrong during the build process', 500);
|
||||
}
|
||||
|
||||
$destinationDevice = getStorageDevice($destination);
|
||||
$outputPath = $destinationDevice->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
|
||||
$buffer = $localDevice->read($tmpBuild);
|
||||
if (!$destinationDevice->write($outputPath, $buffer, $localDevice->getFileMimeType($tmpBuild))) {
|
||||
throw new Exception('Failed to move built code to storage', 500);
|
||||
};
|
||||
|
||||
$container['outputPath'] = $outputPath;
|
||||
}
|
||||
|
||||
if (empty($stdout)) {
|
||||
$stdout = 'Build Successful!';
|
||||
}
|
||||
|
||||
$endTime = DateTime::now();
|
||||
$endTimeUnix = (new \DateTime($endTime))->getTimestamp();
|
||||
$duration = $endTimeUnix - $startTimeUnix;
|
||||
|
||||
$container = array_merge($container, [
|
||||
'status' => 'ready',
|
||||
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
|
||||
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
|
||||
'startTime' => $startTime,
|
||||
'endTime' => $endTime,
|
||||
'duration' => $duration,
|
||||
]);
|
||||
|
||||
|
||||
if (!$remove) {
|
||||
$activeRuntimes->set($runtimeId, [
|
||||
'id' => $containerId,
|
||||
'name' => $runtimeId,
|
||||
'created' => $startTimeUnix,
|
||||
'updated' => $endTimeUnix,
|
||||
'status' => 'Up ' . \round($duration, 2) . 's',
|
||||
'key' => $secret,
|
||||
]);
|
||||
}
|
||||
|
||||
Console::success('Build Stage completed in ' . ($duration) . ' seconds');
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Build failed: ' . $th->getMessage() . $stdout);
|
||||
|
||||
throw new Exception($th->getMessage() . $stdout, 500);
|
||||
} finally {
|
||||
// Container cleanup
|
||||
if ($remove) {
|
||||
if (!empty($containerId)) {
|
||||
// If container properly created
|
||||
$orchestration->remove($containerId, true);
|
||||
$activeRuntimes->del($runtimeId);
|
||||
} else {
|
||||
// If whole creation failed, but container might have been initialized
|
||||
try {
|
||||
// Try to remove with contaier name instead of ID
|
||||
$orchestration->remove($runtimeId, true);
|
||||
$activeRuntimes->del($runtimeId);
|
||||
} catch (Throwable $th) {
|
||||
// If fails, means initialization also failed.
|
||||
// Contianer is not there, no need to remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release orchestration back to pool, we are done with it
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->json($container);
|
||||
});
|
||||
|
||||
|
||||
App::get('/v1/runtimes')
|
||||
->desc("List currently active runtimes")
|
||||
->inject('activeRuntimes')
|
||||
->inject('response')
|
||||
->action(function ($activeRuntimes, Response $response) {
|
||||
$runtimes = [];
|
||||
|
||||
foreach ($activeRuntimes as $runtime) {
|
||||
$runtimes[] = $runtime;
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->json($runtimes);
|
||||
});
|
||||
|
||||
App::get('/v1/runtimes/:runtimeId')
|
||||
->desc("Get a runtime by its ID")
|
||||
->param('runtimeId', '', new Text(64), 'Runtime unique ID.')
|
||||
->inject('activeRuntimes')
|
||||
->inject('response')
|
||||
->action(function ($runtimeId, $activeRuntimes, Response $response) {
|
||||
|
||||
if (!$activeRuntimes->exists($runtimeId)) {
|
||||
throw new Exception('Runtime not found', 404);
|
||||
}
|
||||
|
||||
$runtime = $activeRuntimes->get($runtimeId);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->json($runtime);
|
||||
});
|
||||
|
||||
App::delete('/v1/runtimes/:runtimeId')
|
||||
->desc('Delete a runtime')
|
||||
->param('runtimeId', '', new Text(64), 'Runtime unique ID.', false)
|
||||
->inject('orchestrationPool')
|
||||
->inject('activeRuntimes')
|
||||
->inject('response')
|
||||
->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) {
|
||||
|
||||
if (!$activeRuntimes->exists($runtimeId)) {
|
||||
throw new Exception('Runtime not found', 404);
|
||||
}
|
||||
|
||||
Console::info('Deleting runtime: ' . $runtimeId);
|
||||
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$orchestration->remove($runtimeId, true);
|
||||
$activeRuntimes->del($runtimeId);
|
||||
Console::success('Removed runtime container: ' . $runtimeId);
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
|
||||
// Remove all the build containers with that same ID
|
||||
// TODO:: Delete build containers
|
||||
// foreach ($buildIds as $buildId) {
|
||||
// try {
|
||||
// Console::info('Deleting build container : ' . $buildId);
|
||||
// $status = $orchestration->remove('build-' . $buildId, true);
|
||||
// } catch (Throwable $th) {
|
||||
// Console::error($th->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->send();
|
||||
});
|
||||
|
||||
|
||||
App::post('/v1/execution')
|
||||
->desc('Create an execution')
|
||||
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
|
||||
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
|
||||
->param('data', '', new Text(8192, 0), 'Data to be forwarded to the function, this is user specified.', true)
|
||||
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
|
||||
->inject('activeRuntimes')
|
||||
->inject('response')
|
||||
->action(
|
||||
function (string $runtimeId, array $vars, string $data, $timeout, $activeRuntimes, Response $response) {
|
||||
if (!$activeRuntimes->exists($runtimeId)) {
|
||||
throw new Exception('Runtime not found. Please create the runtime.', 404);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
if ($activeRuntimes->get($runtimeId)['status'] === 'pending') {
|
||||
Console::info('Waiting for runtime to be ready...');
|
||||
sleep(1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($i === 4) {
|
||||
throw new Exception('Runtime failed to launch in allocated time.', 500);
|
||||
}
|
||||
}
|
||||
|
||||
$runtime = $activeRuntimes->get($runtimeId);
|
||||
$secret = $runtime['key'];
|
||||
if (empty($secret)) {
|
||||
throw new Exception('Runtime secret not found. Please re-create the runtime.', 500);
|
||||
}
|
||||
|
||||
Console::info('Executing Runtime: ' . $runtimeId);
|
||||
|
||||
$execution = [];
|
||||
$executionStart = \microtime(true);
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$res = '';
|
||||
$statusCode = 0;
|
||||
$errNo = -1;
|
||||
$executorResponse = '';
|
||||
|
||||
$timeout ??= (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900);
|
||||
|
||||
$ch = \curl_init();
|
||||
$body = \json_encode([
|
||||
'variables' => $vars,
|
||||
'payload' => $data,
|
||||
'timeout' => $timeout
|
||||
]);
|
||||
\curl_setopt($ch, CURLOPT_URL, "http://" . $runtimeId . ":3000/");
|
||||
\curl_setopt($ch, CURLOPT_POST, true);
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
\curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'x-internal-challenge: ' . $secret,
|
||||
'host: null'
|
||||
]);
|
||||
|
||||
$executorResponse = \curl_exec($ch);
|
||||
$executorResponse = json_decode($executorResponse, true);
|
||||
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
$error = \curl_error($ch);
|
||||
|
||||
$errNo = \curl_errno($ch);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
switch (true) {
|
||||
/** No Error. */
|
||||
case $errNo === 0:
|
||||
break;
|
||||
/** Runtime not ready for requests yet. 111 is the swoole error code for Connection Refused - see https://openswoole.com/docs/swoole-error-code */
|
||||
case $errNo === 111:
|
||||
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 406);
|
||||
/** Any other CURL error */
|
||||
default:
|
||||
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500);
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $statusCode >= 500:
|
||||
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.';
|
||||
$stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.';
|
||||
$res = ($executorResponse ?? [])['response'] ?? '';
|
||||
if (is_array($res)) {
|
||||
$res = json_encode($res, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
break;
|
||||
case $statusCode >= 100:
|
||||
$stdout = $executorResponse['stdout'];
|
||||
$res = $executorResponse['response'];
|
||||
if (is_array($res)) {
|
||||
$res = json_encode($res, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.';
|
||||
$stdout = ($executorResponse ?? [])['stdout'] ?? '';
|
||||
break;
|
||||
}
|
||||
|
||||
$executionEnd = \microtime(true);
|
||||
$executionTime = ($executionEnd - $executionStart);
|
||||
$functionStatus = ($statusCode >= 500) ? 'failed' : 'completed';
|
||||
|
||||
Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus);
|
||||
|
||||
$execution = [
|
||||
'status' => $functionStatus,
|
||||
'statusCode' => $statusCode,
|
||||
'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB
|
||||
'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
|
||||
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
|
||||
'duration' => $executionTime,
|
||||
];
|
||||
|
||||
/** Update swoole table */
|
||||
$runtime['updated'] = \time();
|
||||
$activeRuntimes->set($runtimeId, $runtime);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->json($execution);
|
||||
}
|
||||
);
|
||||
|
||||
App::setMode(App::MODE_TYPE_PRODUCTION); // Define Mode
|
||||
|
||||
$http = new Server("0.0.0.0", 80);
|
||||
|
||||
/** Set Resources */
|
||||
App::setResource('orchestrationPool', fn() => $orchestrationPool);
|
||||
App::setResource('activeRuntimes', fn() => $activeRuntimes);
|
||||
|
||||
/** Set callbacks */
|
||||
App::error()
|
||||
->inject('utopia')
|
||||
->inject('error')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (App $utopia, throwable $error, Request $request, Response $response) {
|
||||
$route = $utopia->match($request);
|
||||
logError($error, "httpError", $route);
|
||||
|
||||
switch ($error->getCode()) {
|
||||
case 400: // Error allowed publicly
|
||||
case 401: // Error allowed publicly
|
||||
case 402: // Error allowed publicly
|
||||
case 403: // Error allowed publicly
|
||||
case 404: // Error allowed publicly
|
||||
case 406: // Error allowed publicly
|
||||
case 409: // Error allowed publicly
|
||||
case 412: // Error allowed publicly
|
||||
case 425: // Error allowed publicly
|
||||
case 429: // Error allowed publicly
|
||||
case 501: // Error allowed publicly
|
||||
case 503: // Error allowed publicly
|
||||
$code = $error->getCode();
|
||||
break;
|
||||
default:
|
||||
$code = 500; // All other errors get the generic 500 server error status code
|
||||
}
|
||||
|
||||
$output = [
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
'file' => $error->getFile(),
|
||||
'line' => $error->getLine(),
|
||||
'trace' => $error->getTrace(),
|
||||
'version' => App::getEnv('_APP_VERSION', 'UNKNOWN')
|
||||
];
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->setStatusCode($code);
|
||||
|
||||
$response->json($output);
|
||||
});
|
||||
|
||||
App::init()
|
||||
->inject('request')
|
||||
->action(function (Request $request) {
|
||||
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
|
||||
if (empty($secretKey)) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
|
||||
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
|
||||
throw new Exception('Missing executor key', 401);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$http->on('start', function ($http) {
|
||||
global $orchestrationPool;
|
||||
global $activeRuntimes;
|
||||
|
||||
/**
|
||||
* Warmup: make sure images are ready to run fast 🚀
|
||||
*/
|
||||
$runtimes = new Runtimes('v2');
|
||||
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
|
||||
$runtimes = $runtimes->getAll(true, $allowList);
|
||||
foreach ($runtimes as $runtime) {
|
||||
go(function () use ($runtime, $orchestrationPool) {
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
Console::info('Warming up ' . $runtime['name'] . ' ' . $runtime['version'] . ' environment...');
|
||||
$response = $orchestration->pull($runtime['image']);
|
||||
if ($response) {
|
||||
Console::success("Successfully Warmed up {$runtime['name']} {$runtime['version']}!");
|
||||
} else {
|
||||
Console::warning("Failed to Warmup {$runtime['name']} {$runtime['version']}!");
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove residual runtimes
|
||||
*/
|
||||
Console::info('Removing orphan runtimes...');
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$orphans = $orchestration->list(['label' => 'openruntimes-type=runtime']);
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
|
||||
foreach ($orphans as $runtime) {
|
||||
go(function () use ($runtime, $orchestrationPool) {
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$orchestration->remove($runtime->getName(), true);
|
||||
Console::success("Successfully removed {$runtime->getName()}");
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Orphan runtime deletion failed: ' . $th->getMessage());
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers for shutdown
|
||||
*/
|
||||
@Process::signal(SIGINT, function () use ($http) {
|
||||
$http->shutdown();
|
||||
});
|
||||
|
||||
@Process::signal(SIGQUIT, function () use ($http) {
|
||||
$http->shutdown();
|
||||
});
|
||||
|
||||
@Process::signal(SIGKILL, function () use ($http) {
|
||||
$http->shutdown();
|
||||
});
|
||||
|
||||
@Process::signal(SIGTERM, function () use ($http) {
|
||||
$http->shutdown();
|
||||
});
|
||||
|
||||
/**
|
||||
* Run a maintenance worker every MAINTENANCE_INTERVAL seconds to remove inactive runtimes
|
||||
*/
|
||||
Timer::tick(MAINTENANCE_INTERVAL * 1000, function () use ($orchestrationPool, $activeRuntimes) {
|
||||
Console::warning("Running maintenance task ...");
|
||||
foreach ($activeRuntimes as $runtime) {
|
||||
$inactiveThreshold = \time() - App::getEnv('_APP_FUNCTIONS_INACTIVE_THRESHOLD', 60);
|
||||
if ($runtime['updated'] < $inactiveThreshold) {
|
||||
go(function () use ($runtime, $orchestrationPool, $activeRuntimes) {
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$orchestration->remove($runtime['name'], true);
|
||||
$activeRuntimes->del($runtime['name']);
|
||||
Console::success("Successfully removed {$runtime['name']}");
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Inactive Runtime deletion failed: ' . $th->getMessage());
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$http->on('beforeShutdown', function () {
|
||||
global $orchestrationPool;
|
||||
Console::info('Cleaning up containers before shutdown...');
|
||||
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$functionsToRemove = $orchestration->list(['label' => 'openruntimes-type=runtime']);
|
||||
$orchestrationPool->put($orchestration);
|
||||
|
||||
foreach ($functionsToRemove as $container) {
|
||||
go(function () use ($orchestrationPool, $container) {
|
||||
try {
|
||||
$orchestration = $orchestrationPool->get();
|
||||
$orchestration->remove($container->getId(), true);
|
||||
Console::info('Removed container ' . $container->getName());
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Failed to remove container: ' . $container->getName());
|
||||
} finally {
|
||||
$orchestrationPool->put($orchestration);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) {
|
||||
$request = new Request($swooleRequest);
|
||||
$response = new Response($swooleResponse);
|
||||
$app = new App('UTC');
|
||||
|
||||
try {
|
||||
$app->run($request, $response);
|
||||
} catch (\Throwable $th) {
|
||||
logError($th, "serverError");
|
||||
$swooleResponse->setStatusCode(500);
|
||||
$output = [
|
||||
'message' => 'Error: ' . $th->getMessage(),
|
||||
'code' => 500,
|
||||
'file' => $th->getFile(),
|
||||
'line' => $th->getLine(),
|
||||
'trace' => $th->getTrace()
|
||||
];
|
||||
$swooleResponse->end(\json_encode($output));
|
||||
}
|
||||
});
|
||||
|
||||
$http->start();
|
||||
70
app/http.php
70
app/http.php
|
|
@ -22,6 +22,7 @@ use Utopia\Swoole\Files;
|
|||
use Appwrite\Utopia\Request;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Pools\Group;
|
||||
|
||||
$http = new Server("0.0.0.0", App::getEnv('PORT', 80));
|
||||
|
||||
|
|
@ -60,6 +61,10 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
$app = new App('UTC');
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var Group $pools */
|
||||
App::setResource('pools', fn () => $pools);
|
||||
|
||||
// wait for database to be ready
|
||||
$attempts = 0;
|
||||
$max = 10;
|
||||
|
|
@ -68,8 +73,8 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
do {
|
||||
try {
|
||||
$attempts++;
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$dbForConsole = $app->getResource('dbForConsole');
|
||||
/** @var Utopia\Database\Database $dbForConsole */
|
||||
break; // leave the do-while if successful
|
||||
} catch (\Exception $e) {
|
||||
Console::warning("Database not ready. Retrying connection ({$attempts})...");
|
||||
|
|
@ -80,17 +85,8 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
}
|
||||
} while ($attempts < $max);
|
||||
|
||||
App::setResource('db', fn () => $db);
|
||||
App::setResource('cache', fn () => $redis);
|
||||
|
||||
/** @var Utopia\Database\Database $dbForConsole */
|
||||
$dbForConsole = $app->getResource('dbForConsole');
|
||||
|
||||
Console::success('[Setup] - Server database init started...');
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
|
||||
try {
|
||||
Console::success('[Setup] - Creating database: appwrite...');
|
||||
$dbForConsole->create();
|
||||
|
|
@ -108,7 +104,10 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
$adapter->setup();
|
||||
}
|
||||
|
||||
foreach ($collections as $key => $collection) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$consoleCollections = $collections['console'];
|
||||
foreach ($consoleCollections as $key => $collection) {
|
||||
if (($collection['$collection'] ?? '') !== Database::METADATA) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -148,7 +147,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
$dbForConsole->createCollection($key, $attributes, $indexes);
|
||||
}
|
||||
|
||||
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'), 'bucket_1')) {
|
||||
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDefaultDatabase(), 'bucket_1')) {
|
||||
Console::success('[Setup] - Creating default bucket...');
|
||||
$dbForConsole->createDocument('buckets', new Document([
|
||||
'$id' => ID::custom('default'),
|
||||
|
|
@ -173,7 +172,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
$bucket = $dbForConsole->getDocument('buckets', 'default');
|
||||
|
||||
Console::success('[Setup] - Creating files collection for default bucket...');
|
||||
$files = $collections['files'] ?? [];
|
||||
$files = $collections['buckets']['files'] ?? [];
|
||||
if (empty($files)) {
|
||||
throw new Exception('Files collection is not configured.');
|
||||
}
|
||||
|
|
@ -208,6 +207,8 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
|
||||
}
|
||||
|
||||
$pools->reclaim();
|
||||
|
||||
Console::success('[Setup] - Server database init completed...');
|
||||
});
|
||||
|
||||
|
|
@ -222,28 +223,33 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
|||
});
|
||||
|
||||
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
|
||||
App::setResource('swooleRequest', fn () => $swooleRequest);
|
||||
App::setResource('swooleResponse', fn () => $swooleResponse);
|
||||
|
||||
$request = new Request($swooleRequest);
|
||||
$response = new Response($swooleResponse);
|
||||
|
||||
if (Files::isFileLoaded($request->getURI())) {
|
||||
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
|
||||
// Serve static files (console) only for main domain
|
||||
$host = $request->getHostname() ?? '';
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
if ($host === $mainDomain || $host === 'localhost') {
|
||||
if (Files::isFileLoaded($request->getURI())) {
|
||||
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
|
||||
|
||||
$response
|
||||
->setContentType(Files::getFileMimeType($request->getURI()))
|
||||
->addHeader('Cache-Control', 'public, max-age=' . $time)
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
|
||||
->send(Files::getFileContents($request->getURI()));
|
||||
$response
|
||||
->setContentType(Files::getFileMimeType($request->getURI()))
|
||||
->addHeader('Cache-Control', 'public, max-age=' . $time)
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
|
||||
->send(Files::getFileContents($request->getURI()));
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$app = new App('UTC');
|
||||
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
|
||||
App::setResource('db', fn () => $db);
|
||||
App::setResource('cache', fn () => $redis);
|
||||
$pools = $register->get('pools');
|
||||
App::setResource('pools', fn () => $pools);
|
||||
|
||||
try {
|
||||
Authorization::cleanRoles();
|
||||
|
|
@ -263,7 +269,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
|||
}
|
||||
|
||||
$loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs");
|
||||
$route = $app->match($request);
|
||||
$route = $app->getRoute();
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
|
|
@ -327,13 +333,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
|||
|
||||
$swooleResponse->end(\json_encode($output));
|
||||
} finally {
|
||||
/** @var PDOPool $dbPool */
|
||||
$dbPool = $register->get('dbPool');
|
||||
$dbPool->put($db);
|
||||
|
||||
/** @var RedisPool $redisPool */
|
||||
$redisPool = $register->get('redisPool');
|
||||
$redisPool->put($redis);
|
||||
$pools->reclaim();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
747
app/init.php
747
app/init.php
|
|
@ -18,37 +18,27 @@ ini_set('display_startup_errors', 1);
|
|||
ini_set('default_socket_timeout', -1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\DSN\DSN;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Phone;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\PDO;
|
||||
use Appwrite\GraphQL\Promises\Adapter\Swoole;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use Appwrite\Usage\Stats;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOConfig;
|
||||
use Swoole\Database\PDOPool;
|
||||
use Swoole\Database\RedisConfig;
|
||||
use Swoole\Database\RedisPool;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
|
@ -56,7 +46,9 @@ use Utopia\Database\Validator\Authorization;
|
|||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Messaging\Adapters\SMS\Mock;
|
||||
use Appwrite\GraphQL\Promises\Adapter\Swoole;
|
||||
use Utopia\Messaging\Adapters\SMS\Msg91;
|
||||
use Utopia\Messaging\Adapters\SMS\Telesign;
|
||||
use Utopia\Messaging\Adapters\SMS\TextMagic;
|
||||
|
|
@ -70,7 +62,22 @@ use Utopia\Storage\Device\Linode;
|
|||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Device\S3;
|
||||
use Utopia\Storage\Device\Wasabi;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Adapter\MySQL;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Pools\Pool;
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\OAuth2\Github;
|
||||
use Appwrite\Event\Func;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Queue;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\URL;
|
||||
|
|
@ -99,9 +106,10 @@ const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate
|
|||
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
|
||||
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
|
||||
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 506;
|
||||
const APP_VERSION_STABLE = '1.3.8';
|
||||
const APP_CACHE_BUSTER = 507;
|
||||
const APP_VERSION_STABLE = '1.4.0';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
|
@ -128,6 +136,7 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
|
|||
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
|
||||
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
|
||||
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
|
||||
const APP_HOSTNAME_INTERNAL = 'appwrite';
|
||||
// Database Reconnect
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
|
||||
|
|
@ -151,13 +160,15 @@ const DELETE_TYPE_TEAMS = 'teams';
|
|||
const DELETE_TYPE_EXECUTIONS = 'executions';
|
||||
const DELETE_TYPE_AUDIT = 'audit';
|
||||
const DELETE_TYPE_ABUSE = 'abuse';
|
||||
const DELETE_TYPE_CERTIFICATES = 'certificates';
|
||||
const DELETE_TYPE_USAGE = 'usage';
|
||||
const DELETE_TYPE_REALTIME = 'realtime';
|
||||
const DELETE_TYPE_BUCKETS = 'buckets';
|
||||
const DELETE_TYPE_INSTALLATIONS = 'installations';
|
||||
const DELETE_TYPE_RULES = 'rules';
|
||||
const DELETE_TYPE_SESSIONS = 'sessions';
|
||||
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
|
||||
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
|
||||
const DELETE_TYPE_SCHEDULES = 'schedules';
|
||||
// Compression type
|
||||
const COMPRESSION_TYPE_NONE = 'none';
|
||||
const COMPRESSION_TYPE_GZIP = 'gzip';
|
||||
|
|
@ -175,6 +186,42 @@ const APP_AUTH_TYPE_KEY = 'Key';
|
|||
const APP_AUTH_TYPE_ADMIN = 'Admin';
|
||||
// Response related
|
||||
const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
|
||||
// Function headers
|
||||
const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host'];
|
||||
const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
|
||||
// Usage metrics
|
||||
const METRIC_TEAMS = 'teams';
|
||||
const METRIC_USERS = 'users';
|
||||
const METRIC_SESSIONS = 'sessions';
|
||||
const METRIC_DATABASES = 'databases';
|
||||
const METRIC_COLLECTIONS = 'collections';
|
||||
const METRIC_DATABASE_ID_COLLECTIONS = '{databaseInternalId}.collections';
|
||||
const METRIC_DOCUMENTS = 'documents';
|
||||
const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents';
|
||||
const METRIC_BUCKETS = 'buckets';
|
||||
const METRIC_FILES = 'files';
|
||||
const METRIC_FILES_STORAGE = 'files.storage';
|
||||
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
|
||||
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
|
||||
const METRIC_FUNCTIONS = 'functions';
|
||||
const METRIC_DEPLOYMENTS = 'deployments';
|
||||
const METRIC_DEPLOYMENTS_STORAGE = 'deployments.storage';
|
||||
const METRIC_BUILDS = 'builds';
|
||||
const METRIC_BUILDS_STORAGE = 'builds.storage';
|
||||
const METRIC_BUILDS_COMPUTE = 'builds.compute';
|
||||
const METRIC_FUNCTION_ID_BUILDS = '{functionInternalId}.builds';
|
||||
const METRIC_FUNCTION_ID_BUILDS_STORAGE = '{functionInternalId}.builds.storage';
|
||||
const METRIC_FUNCTION_ID_BUILDS_COMPUTE = '{functionInternalId}.builds.compute';
|
||||
const METRIC_FUNCTION_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
|
||||
const METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
|
||||
const METRIC_EXECUTIONS = 'executions';
|
||||
const METRIC_EXECUTIONS_COMPUTE = 'executions.compute';
|
||||
const METRIC_FUNCTION_ID_EXECUTIONS = '{functionInternalId}.executions';
|
||||
const METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE = '{functionInternalId}.executions.compute';
|
||||
const METRIC_NETWORK_REQUESTS = 'network.requests';
|
||||
const METRIC_NETWORK_INBOUND = 'network.inbound';
|
||||
const METRIC_NETWORK_OUTBOUND = 'network.outbound';
|
||||
|
||||
$register = new Registry();
|
||||
|
||||
|
|
@ -205,6 +252,7 @@ Config::load('locale-languages', __DIR__ . '/config/locale/languages.php');
|
|||
Config::load('locale-phones', __DIR__ . '/config/locale/phones.php');
|
||||
Config::load('locale-countries', __DIR__ . '/config/locale/countries.php');
|
||||
Config::load('locale-continents', __DIR__ . '/config/locale/continents.php');
|
||||
Config::load('locale-templates', __DIR__ . '/config/locale/templates.php');
|
||||
Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
|
||||
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
|
||||
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
|
||||
|
|
@ -245,7 +293,7 @@ Database::addFilter(
|
|||
return $value;
|
||||
},
|
||||
function (mixed $value, Document $attribute) {
|
||||
$formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true);
|
||||
$formatOptions = \json_decode($attribute->getAttribute('formatOptions', '[]'), true);
|
||||
if (isset($formatOptions['elements'])) {
|
||||
$attribute->setAttribute('elements', $formatOptions['elements']);
|
||||
}
|
||||
|
|
@ -315,7 +363,7 @@ Database::addFilter(
|
|||
->find('indexes', [
|
||||
Query::equal('collectionInternalId', [$document->getInternalId()]),
|
||||
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
|
||||
Query::limit(64),
|
||||
Query::limit($database->getLimitForIndexes()),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
|
@ -334,20 +382,6 @@ Database::addFilter(
|
|||
}
|
||||
);
|
||||
|
||||
Database::addFilter(
|
||||
'subQueryDomains',
|
||||
function (mixed $value) {
|
||||
return null;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
->find('domains', [
|
||||
Query::equal('projectInternalId', [$document->getInternalId()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
Database::addFilter(
|
||||
'subQueryKeys',
|
||||
function (mixed $value) {
|
||||
|
|
@ -425,7 +459,8 @@ Database::addFilter(
|
|||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
->find('variables', [
|
||||
Query::equal('functionInternalId', [$document->getInternalId()]),
|
||||
Query::equal('resourceInternalId', [$document->getInternalId()]),
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
}
|
||||
|
|
@ -457,6 +492,44 @@ Database::addFilter(
|
|||
}
|
||||
);
|
||||
|
||||
// READ-ONLY! TO update, write directly to 'variables' collection. After update to vars, make sure to deleteCachedDocument()
|
||||
Database::addFilter(
|
||||
'subQueryProjectVariables',
|
||||
function (mixed $value) {
|
||||
return null;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database
|
||||
->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
Database::addFilter(
|
||||
'userSearch',
|
||||
function (mixed $value, Document $user) {
|
||||
$searchValues = [
|
||||
$user->getId(),
|
||||
$user->getAttribute('email', ''),
|
||||
$user->getAttribute('name', ''),
|
||||
$user->getAttribute('phone', '')
|
||||
];
|
||||
|
||||
foreach ($user->getAttribute('labels', []) as $label) {
|
||||
$searchValues[] = 'label:' . $label;
|
||||
}
|
||||
|
||||
$search = implode(' ', \array_filter($searchValues));
|
||||
|
||||
return $search;
|
||||
},
|
||||
function (mixed $value) {
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* DB Formats
|
||||
*/
|
||||
|
|
@ -513,58 +586,193 @@ $register->set('logger', function () {
|
|||
$adapter = new $classname($providerConfig);
|
||||
return new Logger($adapter);
|
||||
});
|
||||
$register->set('dbPool', function () {
|
||||
// Register DB connection
|
||||
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
||||
$dbPort = App::getEnv('_APP_DB_PORT', '');
|
||||
$dbUser = App::getEnv('_APP_DB_USER', '');
|
||||
$dbPass = App::getEnv('_APP_DB_PASS', '');
|
||||
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
|
||||
$register->set('pools', function () {
|
||||
$group = new Group();
|
||||
|
||||
$pool = new PDOPool(
|
||||
(new PDOConfig())
|
||||
->withHost($dbHost)
|
||||
->withPort($dbPort)
|
||||
->withDbName($dbScheme)
|
||||
->withCharset('utf8mb4')
|
||||
->withUsername($dbUser)
|
||||
->withPassword($dbPass)
|
||||
->withOptions([
|
||||
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||
]),
|
||||
64
|
||||
);
|
||||
$fallbackForDB = 'db_main=' . AppwriteURL::unparse([
|
||||
'scheme' => 'mariadb',
|
||||
'host' => App::getEnv('_APP_DB_HOST', 'mariadb'),
|
||||
'port' => App::getEnv('_APP_DB_PORT', '3306'),
|
||||
'user' => App::getEnv('_APP_DB_USER', ''),
|
||||
'pass' => App::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => App::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
|
||||
'scheme' => 'redis',
|
||||
'host' => App::getEnv('_APP_REDIS_HOST', 'redis'),
|
||||
'port' => App::getEnv('_APP_REDIS_PORT', '6379'),
|
||||
'user' => App::getEnv('_APP_REDIS_USER', ''),
|
||||
'pass' => App::getEnv('_APP_REDIS_PASS', ''),
|
||||
]);
|
||||
|
||||
return $pool;
|
||||
});
|
||||
$register->set('redisPool', function () {
|
||||
$redisHost = App::getEnv('_APP_REDIS_HOST', '');
|
||||
$redisPort = App::getEnv('_APP_REDIS_PORT', '');
|
||||
$redisUser = App::getEnv('_APP_REDIS_USER', '');
|
||||
$redisPass = App::getEnv('_APP_REDIS_PASS', '');
|
||||
$redisAuth = '';
|
||||
$connections = [
|
||||
'console' => [
|
||||
'type' => 'database',
|
||||
'dsns' => App::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB),
|
||||
'multiple' => false,
|
||||
'schemes' => ['mariadb', 'mysql'],
|
||||
],
|
||||
'database' => [
|
||||
'type' => 'database',
|
||||
'dsns' => App::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB),
|
||||
'multiple' => true,
|
||||
'schemes' => ['mariadb', 'mysql'],
|
||||
],
|
||||
'queue' => [
|
||||
'type' => 'queue',
|
||||
'dsns' => App::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis),
|
||||
'multiple' => false,
|
||||
'schemes' => ['redis'],
|
||||
],
|
||||
'pubsub' => [
|
||||
'type' => 'pubsub',
|
||||
'dsns' => App::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis),
|
||||
'multiple' => false,
|
||||
'schemes' => ['redis'],
|
||||
],
|
||||
'cache' => [
|
||||
'type' => 'cache',
|
||||
'dsns' => App::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis),
|
||||
'multiple' => true,
|
||||
'schemes' => ['redis'],
|
||||
],
|
||||
];
|
||||
|
||||
if ($redisUser && $redisPass) {
|
||||
$redisAuth = $redisUser . ':' . $redisPass;
|
||||
$maxConnections = App::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / App::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$multiprocessing = App::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
|
||||
|
||||
if ($multiprocessing) {
|
||||
$workerCount = swoole_cpu_num() * intval(App::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
} else {
|
||||
$workerCount = 1;
|
||||
}
|
||||
|
||||
$pool = new RedisPool(
|
||||
(new RedisConfig())
|
||||
->withHost($redisHost)
|
||||
->withPort($redisPort)
|
||||
->withAuth($redisAuth)
|
||||
->withDbIndex(0),
|
||||
64
|
||||
);
|
||||
if ($workerCount > $instanceConnections) {
|
||||
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
|
||||
}
|
||||
|
||||
return $pool;
|
||||
$poolSize = (int)($instanceConnections / $workerCount);
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
$dsns = $connection['dsns'] ?? '';
|
||||
$multipe = $connection['multiple'] ?? false;
|
||||
$schemes = $connection['schemes'] ?? [];
|
||||
$config = [];
|
||||
$dsns = explode(',', $connection['dsns'] ?? '');
|
||||
foreach ($dsns as &$dsn) {
|
||||
$dsn = explode('=', $dsn);
|
||||
$name = ($multipe) ? $key . '_' . $dsn[0] : $key;
|
||||
$dsn = $dsn[1] ?? '';
|
||||
$config[] = $name;
|
||||
if (empty($dsn)) {
|
||||
//throw new Exception(Exception::GENERAL_SERVER_ERROR, "Missing value for DSN connection in {$key}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$dsn = new DSN($dsn);
|
||||
$dsnHost = $dsn->getHost();
|
||||
$dsnPort = $dsn->getPort();
|
||||
$dsnUser = $dsn->getUser();
|
||||
$dsnPass = $dsn->getPassword();
|
||||
$dsnScheme = $dsn->getScheme();
|
||||
$dsnDatabase = $dsn->getPath();
|
||||
|
||||
if (!in_array($dsnScheme, $schemes)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid console database scheme");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Resource
|
||||
*
|
||||
* Creation could be reused accross connection types like database, cache, queue, etc.
|
||||
*
|
||||
* Resource assignment to an adapter will happen below.
|
||||
*/
|
||||
switch ($dsnScheme) {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
$resource = function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true
|
||||
));
|
||||
});
|
||||
};
|
||||
break;
|
||||
case 'redis':
|
||||
$resource = function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
$redis = new Redis();
|
||||
@$redis->pconnect($dsnHost, (int)$dsnPort);
|
||||
if ($dsnPass) {
|
||||
$redis->auth($dsnPass);
|
||||
}
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Invalid scheme");
|
||||
break;
|
||||
}
|
||||
|
||||
$pool = new Pool($name, $poolSize, function () use ($type, $resource, $dsn) {
|
||||
// Get Adapter
|
||||
$adapter = null;
|
||||
switch ($type) {
|
||||
case 'database':
|
||||
$adapter = match ($dsn->getScheme()) {
|
||||
'mariadb' => new MariaDB($resource()),
|
||||
'mysql' => new MySQL($resource()),
|
||||
default => null
|
||||
};
|
||||
|
||||
$adapter->setDefaultDatabase($dsn->getPath());
|
||||
break;
|
||||
case 'pubsub':
|
||||
$adapter = $resource();
|
||||
break;
|
||||
case 'queue':
|
||||
$adapter = match ($dsn->getScheme()) {
|
||||
'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()),
|
||||
default => null
|
||||
};
|
||||
break;
|
||||
case 'cache':
|
||||
$adapter = match ($dsn->getScheme()) {
|
||||
'redis' => new RedisCache($resource()),
|
||||
default => null
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
|
||||
break;
|
||||
}
|
||||
|
||||
return $adapter;
|
||||
});
|
||||
|
||||
$group->add($pool);
|
||||
}
|
||||
|
||||
Config::setParam('pools-' . $key, $config);
|
||||
}
|
||||
|
||||
return $group;
|
||||
});
|
||||
|
||||
$register->set('influxdb', function () {
|
||||
|
||||
// Register DB connection
|
||||
$host = App::getEnv('_APP_INFLUXDB_HOST', '');
|
||||
$port = App::getEnv('_APP_INFLUXDB_PORT', '');
|
||||
|
|
@ -579,7 +787,7 @@ $register->set('influxdb', function () {
|
|||
return $client;
|
||||
});
|
||||
$register->set('statsd', function () {
|
||||
// Register DB connection
|
||||
// Register DB connection
|
||||
$host = App::getEnv('_APP_STATSD_HOST', 'telegraf');
|
||||
$port = App::getEnv('_APP_STATSD_PORT', 8125);
|
||||
|
||||
|
|
@ -593,16 +801,16 @@ $register->set('smtp', function () {
|
|||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = App::getEnv('_APP_SMTP_USERNAME', null);
|
||||
$password = App::getEnv('_APP_SMTP_PASSWORD', null);
|
||||
$username = App::getEnv('_APP_SMTP_USERNAME');
|
||||
$password = App::getEnv('_APP_SMTP_PASSWORD');
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = App::getEnv('_APP_SMTP_HOST', 'smtp');
|
||||
$mail->Port = App::getEnv('_APP_SMTP_PORT', 25);
|
||||
$mail->SMTPAuth = (!empty($username) && !empty($password));
|
||||
$mail->SMTPAuth = !empty($username) && !empty($password);
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = App::getEnv('_APP_SMTP_SECURE', false);
|
||||
$mail->SMTPSecure = App::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
|
|
@ -625,113 +833,30 @@ $register->set('passwordsDictionary', function () {
|
|||
$content = array_flip($content);
|
||||
return $content;
|
||||
});
|
||||
$register->set('db', function () {
|
||||
// This is usually for our workers or CLI commands scope
|
||||
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
||||
$dbPort = App::getEnv('_APP_DB_PORT', '');
|
||||
$dbUser = App::getEnv('_APP_DB_USER', '');
|
||||
$dbPass = App::getEnv('_APP_DB_PASS', '');
|
||||
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
|
||||
|
||||
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
|
||||
PDO::ATTR_TIMEOUT => 3, // Seconds
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => true,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => true,
|
||||
));
|
||||
|
||||
return $pdo;
|
||||
});
|
||||
$register->set('cache', function () {
|
||||
// This is usually for our workers or CLI commands scope
|
||||
$redis = new Redis();
|
||||
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
return $redis;
|
||||
});
|
||||
$register->set('promiseAdapter', function () {
|
||||
return new Swoole();
|
||||
});
|
||||
|
||||
/*
|
||||
* Localization
|
||||
*/
|
||||
Locale::$exceptions = false;
|
||||
Locale::setLanguageFromJSON('af', __DIR__ . '/config/locale/translations/af.json');
|
||||
Locale::setLanguageFromJSON('ar', __DIR__ . '/config/locale/translations/ar.json');
|
||||
Locale::setLanguageFromJSON('as', __DIR__ . '/config/locale/translations/as.json');
|
||||
Locale::setLanguageFromJSON('az', __DIR__ . '/config/locale/translations/az.json');
|
||||
Locale::setLanguageFromJSON('be', __DIR__ . '/config/locale/translations/be.json');
|
||||
Locale::setLanguageFromJSON('bg', __DIR__ . '/config/locale/translations/bg.json');
|
||||
Locale::setLanguageFromJSON('bh', __DIR__ . '/config/locale/translations/bh.json');
|
||||
Locale::setLanguageFromJSON('bn', __DIR__ . '/config/locale/translations/bn.json');
|
||||
Locale::setLanguageFromJSON('bs', __DIR__ . '/config/locale/translations/bs.json');
|
||||
Locale::setLanguageFromJSON('ca', __DIR__ . '/config/locale/translations/ca.json');
|
||||
Locale::setLanguageFromJSON('cs', __DIR__ . '/config/locale/translations/cs.json');
|
||||
Locale::setLanguageFromJSON('da', __DIR__ . '/config/locale/translations/da.json');
|
||||
Locale::setLanguageFromJSON('de', __DIR__ . '/config/locale/translations/de.json');
|
||||
Locale::setLanguageFromJSON('el', __DIR__ . '/config/locale/translations/el.json');
|
||||
Locale::setLanguageFromJSON('en', __DIR__ . '/config/locale/translations/en.json');
|
||||
Locale::setLanguageFromJSON('eo', __DIR__ . '/config/locale/translations/eo.json');
|
||||
Locale::setLanguageFromJSON('es', __DIR__ . '/config/locale/translations/es.json');
|
||||
Locale::setLanguageFromJSON('fa', __DIR__ . '/config/locale/translations/fa.json');
|
||||
Locale::setLanguageFromJSON('fi', __DIR__ . '/config/locale/translations/fi.json');
|
||||
Locale::setLanguageFromJSON('fo', __DIR__ . '/config/locale/translations/fo.json');
|
||||
Locale::setLanguageFromJSON('fr', __DIR__ . '/config/locale/translations/fr.json');
|
||||
Locale::setLanguageFromJSON('ga', __DIR__ . '/config/locale/translations/ga.json');
|
||||
Locale::setLanguageFromJSON('gu', __DIR__ . '/config/locale/translations/gu.json');
|
||||
Locale::setLanguageFromJSON('he', __DIR__ . '/config/locale/translations/he.json');
|
||||
Locale::setLanguageFromJSON('hi', __DIR__ . '/config/locale/translations/hi.json');
|
||||
Locale::setLanguageFromJSON('hr', __DIR__ . '/config/locale/translations/hr.json');
|
||||
Locale::setLanguageFromJSON('hu', __DIR__ . '/config/locale/translations/hu.json');
|
||||
Locale::setLanguageFromJSON('hy', __DIR__ . '/config/locale/translations/hy.json');
|
||||
Locale::setLanguageFromJSON('id', __DIR__ . '/config/locale/translations/id.json');
|
||||
Locale::setLanguageFromJSON('is', __DIR__ . '/config/locale/translations/is.json');
|
||||
Locale::setLanguageFromJSON('it', __DIR__ . '/config/locale/translations/it.json');
|
||||
Locale::setLanguageFromJSON('ja', __DIR__ . '/config/locale/translations/ja.json');
|
||||
Locale::setLanguageFromJSON('jv', __DIR__ . '/config/locale/translations/jv.json');
|
||||
Locale::setLanguageFromJSON('kn', __DIR__ . '/config/locale/translations/kn.json');
|
||||
Locale::setLanguageFromJSON('km', __DIR__ . '/config/locale/translations/km.json');
|
||||
Locale::setLanguageFromJSON('ko', __DIR__ . '/config/locale/translations/ko.json');
|
||||
Locale::setLanguageFromJSON('la', __DIR__ . '/config/locale/translations/la.json');
|
||||
Locale::setLanguageFromJSON('lb', __DIR__ . '/config/locale/translations/lb.json');
|
||||
Locale::setLanguageFromJSON('lt', __DIR__ . '/config/locale/translations/lt.json');
|
||||
Locale::setLanguageFromJSON('lv', __DIR__ . '/config/locale/translations/lv.json');
|
||||
Locale::setLanguageFromJSON('ml', __DIR__ . '/config/locale/translations/ml.json');
|
||||
Locale::setLanguageFromJSON('mr', __DIR__ . '/config/locale/translations/mr.json');
|
||||
Locale::setLanguageFromJSON('ms', __DIR__ . '/config/locale/translations/ms.json');
|
||||
Locale::setLanguageFromJSON('nb', __DIR__ . '/config/locale/translations/nb.json');
|
||||
Locale::setLanguageFromJSON('ne', __DIR__ . '/config/locale/translations/ne.json');
|
||||
Locale::setLanguageFromJSON('nl', __DIR__ . '/config/locale/translations/nl.json');
|
||||
Locale::setLanguageFromJSON('nn', __DIR__ . '/config/locale/translations/nn.json');
|
||||
Locale::setLanguageFromJSON('or', __DIR__ . '/config/locale/translations/or.json');
|
||||
Locale::setLanguageFromJSON('pa', __DIR__ . '/config/locale/translations/pa.json');
|
||||
Locale::setLanguageFromJSON('pl', __DIR__ . '/config/locale/translations/pl.json');
|
||||
Locale::setLanguageFromJSON('pt-br', __DIR__ . '/config/locale/translations/pt-br.json');
|
||||
Locale::setLanguageFromJSON('pt-pt', __DIR__ . '/config/locale/translations/pt-pt.json');
|
||||
Locale::setLanguageFromJSON('ro', __DIR__ . '/config/locale/translations/ro.json');
|
||||
Locale::setLanguageFromJSON('ru', __DIR__ . '/config/locale/translations/ru.json');
|
||||
Locale::setLanguageFromJSON('sa', __DIR__ . '/config/locale/translations/sa.json');
|
||||
Locale::setLanguageFromJSON('sd', __DIR__ . '/config/locale/translations/sd.json');
|
||||
Locale::setLanguageFromJSON('si', __DIR__ . '/config/locale/translations/si.json');
|
||||
Locale::setLanguageFromJSON('sk', __DIR__ . '/config/locale/translations/sk.json');
|
||||
Locale::setLanguageFromJSON('sl', __DIR__ . '/config/locale/translations/sl.json');
|
||||
Locale::setLanguageFromJSON('sn', __DIR__ . '/config/locale/translations/sn.json');
|
||||
Locale::setLanguageFromJSON('sq', __DIR__ . '/config/locale/translations/sq.json');
|
||||
Locale::setLanguageFromJSON('sv', __DIR__ . '/config/locale/translations/sv.json');
|
||||
Locale::setLanguageFromJSON('ta', __DIR__ . '/config/locale/translations/ta.json');
|
||||
Locale::setLanguageFromJSON('te', __DIR__ . '/config/locale/translations/te.json');
|
||||
Locale::setLanguageFromJSON('th', __DIR__ . '/config/locale/translations/th.json');
|
||||
Locale::setLanguageFromJSON('tl', __DIR__ . '/config/locale/translations/tl.json');
|
||||
Locale::setLanguageFromJSON('tr', __DIR__ . '/config/locale/translations/tr.json');
|
||||
Locale::setLanguageFromJSON('uk', __DIR__ . '/config/locale/translations/uk.json');
|
||||
Locale::setLanguageFromJSON('ur', __DIR__ . '/config/locale/translations/ur.json');
|
||||
Locale::setLanguageFromJSON('vi', __DIR__ . '/config/locale/translations/vi.json');
|
||||
Locale::setLanguageFromJSON('zh-cn', __DIR__ . '/config/locale/translations/zh-cn.json');
|
||||
Locale::setLanguageFromJSON('zh-tw', __DIR__ . '/config/locale/translations/zh-tw.json');
|
||||
|
||||
$locales = Config::getParam('locale-codes', []);
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$code = $locale['code'];
|
||||
|
||||
$path = __DIR__ . '/config/locale/translations/' . $code . '.json';
|
||||
|
||||
if (!\file_exists($path)) {
|
||||
$path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
|
||||
if (!\file_exists($path)) {
|
||||
$path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json`
|
||||
}
|
||||
}
|
||||
|
||||
Locale::setLanguageFromJSON($code, $path);
|
||||
}
|
||||
|
||||
\stream_context_set_default([ // Set global user agent and http settings
|
||||
'http' => [
|
||||
|
|
@ -755,9 +880,12 @@ App::setResource('loggerBreadcrumbs', function () {
|
|||
});
|
||||
|
||||
App::setResource('register', fn() => $register);
|
||||
|
||||
App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')));
|
||||
|
||||
App::setResource('localeCodes', function () {
|
||||
return array_map(fn($locale) => $locale['code'], Config::getParam('locale-codes', []));
|
||||
});
|
||||
|
||||
// Queues
|
||||
App::setResource('events', fn() => new Event('', ''));
|
||||
App::setResource('audits', fn() => new Audit());
|
||||
|
|
@ -765,10 +893,15 @@ App::setResource('mails', fn() => new Mail());
|
|||
App::setResource('deletes', fn() => new Delete());
|
||||
App::setResource('database', fn() => new EventDatabase());
|
||||
App::setResource('messaging', fn() => new Phone());
|
||||
App::setResource('queue', function (Group $pools) {
|
||||
return $pools->get('queue')->pop()->getResource();
|
||||
}, ['pools']);
|
||||
App::setResource('queueForFunctions', function (Connection $queue) {
|
||||
return new Func($queue);
|
||||
}, ['queue']);
|
||||
App::setResource('usage', function ($register) {
|
||||
return new Stats($register->get('statsd'));
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('clients', function ($request, $console, $project) {
|
||||
$console->setAttribute('platforms', [ // Always allow current host
|
||||
'$collection' => ID::custom('platforms'),
|
||||
|
|
@ -849,9 +982,13 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
|||
|
||||
if (APP_MODE_ADMIN !== $mode) {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
|
||||
$user = new Document([]);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', Auth::$unique);
|
||||
if ($project->getId() === 'console') {
|
||||
$user = $dbForConsole->getDocument('users', Auth::$unique);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', Auth::$unique);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$user = $dbForConsole->getDocument('users', Auth::$unique);
|
||||
|
|
@ -861,14 +998,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
|||
$user->isEmpty() // Check a document has been found in the DB
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration)
|
||||
) { // Validate user has valid login token
|
||||
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
|
||||
$user = new Document([]);
|
||||
}
|
||||
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) {
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
|
||||
} else {
|
||||
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
|
||||
$user = new Document([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -891,7 +1028,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
|||
}
|
||||
|
||||
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
|
||||
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
|
||||
$user = new Document([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -905,7 +1042,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
|
|||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
|
||||
|
||||
if ($projectId === 'console') {
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
|
||||
|
|
@ -940,34 +1077,98 @@ App::setResource('console', function () {
|
|||
'legalAddress' => '',
|
||||
'legalTaxId' => '',
|
||||
'auths' => [
|
||||
'invites' => App::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
|
||||
'limit' => (App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
],
|
||||
'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
|
||||
'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
|
||||
'authProviders' => [
|
||||
'githubEnabled' => true,
|
||||
'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
|
||||
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
|
||||
],
|
||||
]);
|
||||
}, []);
|
||||
|
||||
App::setResource('dbForProject', function ($db, $cache, Document $project) {
|
||||
$cache = new Cache(new RedisCache($cache));
|
||||
App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace("_{$project->getInternalId()}");
|
||||
$dbAdapter = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
return $database;
|
||||
}, ['db', 'cache', 'project']);
|
||||
}, ['pools', 'dbForConsole', 'cache', 'project']);
|
||||
|
||||
App::setResource('dbForConsole', function ($db, $cache) {
|
||||
$cache = new Cache(new RedisCache($cache));
|
||||
App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
|
||||
$dbAdapter = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace('_console');
|
||||
|
||||
return $database;
|
||||
}, ['db', 'cache']);
|
||||
}, ['pools', 'cache']);
|
||||
|
||||
App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
$getProjectDB = function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$databaseName = $project->getAttribute('database');
|
||||
|
||||
if (isset($databases[$databaseName])) {
|
||||
$database = $databases[$databaseName];
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
return $database;
|
||||
}
|
||||
|
||||
$dbAdapter = $pools
|
||||
->get($databaseName)
|
||||
->pop()
|
||||
->getResource();
|
||||
|
||||
$database = new Database($dbAdapter, $cache);
|
||||
|
||||
$databases[$databaseName] = $database;
|
||||
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
return $getProjectDB;
|
||||
}, ['pools', 'dbForConsole', 'cache']);
|
||||
|
||||
App::setResource('cache', function (Group $pools) {
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = $pools
|
||||
->get($value)
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['pools']);
|
||||
|
||||
App::setResource('deviceLocal', function () {
|
||||
return new Local();
|
||||
|
|
@ -987,45 +1188,83 @@ App::setResource('deviceBuilds', function ($project) {
|
|||
|
||||
function getDevice($root): Device
|
||||
{
|
||||
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
case Storage::DEVICE_S3:
|
||||
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
|
||||
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
|
||||
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
|
||||
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
|
||||
$s3Acl = 'private';
|
||||
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
|
||||
case Storage::DEVICE_DO_SPACES:
|
||||
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
|
||||
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
|
||||
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
|
||||
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
|
||||
$doSpacesAcl = 'private';
|
||||
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
|
||||
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
|
||||
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
|
||||
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
|
||||
$backblazeAcl = 'private';
|
||||
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
|
||||
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
|
||||
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
|
||||
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
|
||||
$linodeAcl = 'private';
|
||||
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
|
||||
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
|
||||
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
|
||||
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
|
||||
$wasabiAcl = 'private';
|
||||
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
|
||||
$connection = App::getEnv('_APP_CONNECTIONS_STORAGE', '');
|
||||
|
||||
if (!empty($connection)) {
|
||||
$acl = 'private';
|
||||
$device = Storage::DEVICE_LOCAL;
|
||||
$accessKey = '';
|
||||
$accessSecret = '';
|
||||
$bucket = '';
|
||||
$region = '';
|
||||
|
||||
try {
|
||||
$dsn = new DSN($connection);
|
||||
$device = $dsn->getScheme();
|
||||
$accessKey = $dsn->getUser() ?? '';
|
||||
$accessSecret = $dsn->getPassword() ?? '';
|
||||
$bucket = $dsn->getPath() ?? '';
|
||||
$region = $dsn->getParam('region');
|
||||
} catch (\Exception $e) {
|
||||
Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.');
|
||||
}
|
||||
|
||||
switch ($device) {
|
||||
case Storage::DEVICE_S3:
|
||||
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case STORAGE::DEVICE_DO_SPACES:
|
||||
return new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
return new Linode($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
return new Wasabi($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
}
|
||||
} else {
|
||||
switch (strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
case Storage::DEVICE_S3:
|
||||
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
|
||||
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
|
||||
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
|
||||
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
|
||||
$s3Acl = 'private';
|
||||
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
|
||||
case Storage::DEVICE_DO_SPACES:
|
||||
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
|
||||
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
|
||||
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
|
||||
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
|
||||
$doSpacesAcl = 'private';
|
||||
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
|
||||
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
|
||||
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
|
||||
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
|
||||
$backblazeAcl = 'private';
|
||||
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
|
||||
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
|
||||
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
|
||||
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
|
||||
$linodeAcl = 'private';
|
||||
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
|
||||
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
|
||||
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
|
||||
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
|
||||
$wasabiAcl = 'private';
|
||||
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1085,7 +1324,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
|
|||
|
||||
$complexity = function (int $complexity, array $args) {
|
||||
$queries = Query::parseQueries($args['queries'] ?? []);
|
||||
$query = Query::getByType($queries, Query::TYPE_LIMIT)[0] ?? null;
|
||||
$query = Query::getByType($queries, [Query::TYPE_LIMIT])[0] ?? null;
|
||||
$limit = $query ? $query->getValue() : APP_LIMIT_LIST_DEFAULT;
|
||||
|
||||
return $complexity * $limit;
|
||||
|
|
@ -1167,6 +1406,28 @@ App::setResource('schema', function ($utopia, $dbForProject) {
|
|||
);
|
||||
}, ['utopia', 'dbForProject']);
|
||||
|
||||
App::setResource('contributors', function () {
|
||||
$path = 'app/config/contributors.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('employees', function () {
|
||||
$path = 'app/config/employees.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('heroes', function () {
|
||||
$path = 'app/config/heroes.json';
|
||||
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
|
||||
return $list;
|
||||
});
|
||||
|
||||
App::setResource('gitHub', function (Cache $cache) {
|
||||
return new VcsGitHub($cache);
|
||||
}, ['cache']);
|
||||
|
||||
App::setResource('requestTimestamp', function ($request) {
|
||||
//TODO: Move this to the Request class itself
|
||||
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ foreach (
|
|||
realpath(__DIR__ . '/../vendor/symfony'),
|
||||
realpath(__DIR__ . '/../vendor/mongodb'),
|
||||
realpath(__DIR__ . '/../vendor/utopia-php/websocket'), // TODO: remove workerman autoload
|
||||
realpath(__DIR__ . '/../vendor/utopia-php/cache'), // TODO: remove memcache autoload
|
||||
realpath(__DIR__ . '/../vendor/utopia-php/cache'), // TODO: remove memcached autoload
|
||||
realpath(__DIR__ . '/../vendor/utopia-php/queue/src/Queue/Adapter/Workerman.php'), // TODO: remove workerman autoload
|
||||
] as $key => $value
|
||||
) {
|
||||
if ($value !== false) {
|
||||
|
|
|
|||
198
app/realtime.php
198
app/realtime.php
|
|
@ -16,16 +16,15 @@ use Utopia\CLI\Console;
|
|||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Registry\Registry;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\WebSocket\Server;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
|
||||
|
|
@ -33,6 +32,69 @@ require_once __DIR__ . '/init.php';
|
|||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
function getConsoleDB(): Database
|
||||
{
|
||||
global $register;
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $register->get('pools');
|
||||
|
||||
$dbAdapter = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$database = new Database($dbAdapter, getCache());
|
||||
|
||||
$database->setNamespace('_console');
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
function getProjectDB(Document $project): Database
|
||||
{
|
||||
global $register;
|
||||
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
$pools = $register->get('pools');
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return getConsoleDB();
|
||||
}
|
||||
|
||||
$dbAdapter = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$database = new Database($dbAdapter, getCache());
|
||||
$database->setNamespace('_' . $project->getInternalId());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
function getCache(): Cache
|
||||
{
|
||||
global $register;
|
||||
|
||||
$pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */
|
||||
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = $pools
|
||||
->get($value)
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}
|
||||
|
||||
$realtime = new Realtime();
|
||||
|
||||
/**
|
||||
|
|
@ -95,45 +157,6 @@ $logError = function (Throwable $error, string $action) use ($register) {
|
|||
|
||||
$server->error($logError);
|
||||
|
||||
function getDatabase(Registry &$register, string $namespace)
|
||||
{
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
$attempts++;
|
||||
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace($namespace);
|
||||
|
||||
if (!$database->exists($database->getDefaultDatabase(), 'realtime')) {
|
||||
throw new Exception('Collection not ready');
|
||||
}
|
||||
|
||||
break; // leave loop if successful
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning("Database not ready. Retrying connection ({$attempts})...");
|
||||
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
|
||||
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
|
||||
}
|
||||
sleep(DATABASE_RECONNECT_SLEEP);
|
||||
}
|
||||
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
|
||||
|
||||
return [
|
||||
$database,
|
||||
function () use ($register, $db, $redis) {
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
|
@ -143,7 +166,8 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
|||
*/
|
||||
go(function () use ($register, $containerId, &$statsDocument) {
|
||||
$attempts = 0;
|
||||
[$database, $returnDatabase] = getDatabase($register, '_console');
|
||||
$database = getConsoleDB();
|
||||
|
||||
do {
|
||||
try {
|
||||
$attempts++;
|
||||
|
|
@ -163,7 +187,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
|||
sleep(DATABASE_RECONNECT_SLEEP);
|
||||
}
|
||||
} while (true);
|
||||
call_user_func($returnDatabase);
|
||||
$register->get('pools')->reclaim();
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -179,7 +203,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
|||
}
|
||||
|
||||
try {
|
||||
[$database, $returnDatabase] = getDatabase($register, '_console');
|
||||
$database = getConsoleDB();
|
||||
|
||||
$statsDocument
|
||||
->setAttribute('timestamp', DateTime::now())
|
||||
|
|
@ -189,7 +213,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
|||
} catch (\Throwable $th) {
|
||||
call_user_func($logError, $th, "updateWorkerDocument");
|
||||
} finally {
|
||||
call_user_func($returnDatabase);
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -205,7 +229,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) {
|
||||
[$database, $returnDatabase] = getDatabase($register, '_console');
|
||||
$database = getConsoleDB();
|
||||
|
||||
$payload = [];
|
||||
|
||||
|
|
@ -250,7 +274,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
]));
|
||||
}
|
||||
|
||||
call_user_func($returnDatabase);
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
/**
|
||||
* Sending test message for SDK E2E tests every 5 seconds.
|
||||
|
|
@ -285,8 +309,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
}
|
||||
$start = time();
|
||||
|
||||
/** @var Redis $redis */
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
if ($redis->ping(true)) {
|
||||
|
|
@ -305,9 +328,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
|
||||
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
|
||||
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
|
||||
[$consoleDatabase, $returnConsoleDatabase] = getDatabase($register, '_console');
|
||||
$project = Authorization::skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
[$database, $returnDatabase] = getDatabase($register, "_{$project->getInternalId()}");
|
||||
$consoleDatabase = getConsoleDB();
|
||||
$project = Authorization::skip(fn() => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
|
|
@ -315,8 +338,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
|
||||
$realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']);
|
||||
|
||||
call_user_func($returnDatabase);
|
||||
call_user_func($returnConsoleDatabase);
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -344,10 +366,11 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
|||
call_user_func($logError, $th, "pubSubConnection");
|
||||
|
||||
Console::error('Pub/sub error: ' . $th->getMessage());
|
||||
$register->get('redisPool')->put($redis);
|
||||
$attempts++;
|
||||
sleep(DATABASE_RECONNECT_SLEEP);
|
||||
continue;
|
||||
} finally {
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -359,33 +382,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
|
||||
/** @var PDO $db */
|
||||
$db = $register->get('dbPool')->get();
|
||||
/** @var Redis $redis */
|
||||
$redis = $register->get('redisPool')->get();
|
||||
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
App::setResource('db', fn () => $db);
|
||||
App::setResource('cache', fn () => $redis);
|
||||
App::setResource('request', fn () => $request);
|
||||
App::setResource('response', fn () => $response);
|
||||
App::setResource('pools', fn() => $register->get('pools'));
|
||||
App::setResource('request', fn() => $request);
|
||||
App::setResource('response', fn() => $response);
|
||||
|
||||
try {
|
||||
/** @var \Utopia\Database\Document $user */
|
||||
$user = $app->getResource('user');
|
||||
|
||||
/** @var \Utopia\Database\Document $project */
|
||||
$project = $app->getResource('project');
|
||||
|
||||
/** @var \Utopia\Database\Document $console */
|
||||
$console = $app->getResource('console');
|
||||
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace("_{$project->getInternalId()}");
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
*/
|
||||
|
|
@ -393,12 +399,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
throw new Exception('Missing or unknown project ID', 1008);
|
||||
}
|
||||
|
||||
$dbForProject = getProjectDB($project);
|
||||
$console = $app->getResource('console'); /** @var \Utopia\Database\Document $console */
|
||||
$user = $app->getResource('user'); /** @var \Utopia\Database\Document $user */
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
* Abuse limits are connecting 128 times per minute and ip address.
|
||||
*/
|
||||
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $database);
|
||||
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getURI());
|
||||
|
|
@ -469,16 +479,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
}
|
||||
|
||||
if ($th instanceof PDOException) {
|
||||
$db = null;
|
||||
}
|
||||
} finally {
|
||||
/**
|
||||
* Put used PDO and Redis Connections back into their pools.
|
||||
*/
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -486,16 +488,14 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
|||
try {
|
||||
$app = new App('UTC');
|
||||
$response = new Response(new SwooleResponse());
|
||||
$db = $register->get('dbPool')->get();
|
||||
$redis = $register->get('redisPool')->get();
|
||||
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
$database = new Database(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace("_console");
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
$project = $projectId === 'console' ? $app->getResource('console') : Authorization::skip(fn () => $database->getDocument('projects', $projectId));
|
||||
$database->setNamespace("_{$project->getInternalId()}");
|
||||
$database = getConsoleDB();
|
||||
|
||||
if ($projectId !== 'console') {
|
||||
$project = Authorization::skip(fn() => $database->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
|
|
@ -561,7 +561,6 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
|||
|
||||
default:
|
||||
throw new Exception('Message type is not valid.', 1003);
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$response = [
|
||||
|
|
@ -578,8 +577,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
|||
$server->close($connection, $th->getCode());
|
||||
}
|
||||
} finally {
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
$register->get('pools')->reclaim();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
global $cli;
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Validator\Hostname;
|
||||
|
||||
$cli
|
||||
->task('ssl')
|
||||
->desc('Validate server certificates')
|
||||
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
|
||||
->action(function ($domain) {
|
||||
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
|
||||
|
||||
(new Certificate())
|
||||
->setDomain(new Document([
|
||||
'domain' => $domain
|
||||
]))
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
});
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
<?php
|
||||
|
||||
global $cli, $register;
|
||||
|
||||
use Appwrite\Usage\Calculators\TimeSeries;
|
||||
use InfluxDB\Database as InfluxDatabase;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Adapter\MariaDB;
|
||||
use Utopia\Database\Database as UtopiaDatabase;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Authorization::disable();
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
function getDatabase(Registry &$register, string $namespace): UtopiaDatabase
|
||||
{
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
$attempts++;
|
||||
|
||||
$db = $register->get('db');
|
||||
$redis = $register->get('cache');
|
||||
|
||||
$cache = new Cache(new RedisCache($redis));
|
||||
$database = new UtopiaDatabase(new MariaDB($db), $cache);
|
||||
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
|
||||
$database->setNamespace($namespace);
|
||||
|
||||
if (!$database->exists($database->getDefaultDatabase(), 'projects')) {
|
||||
throw new Exception('Projects collection not ready');
|
||||
}
|
||||
break; // leave loop if successful
|
||||
} catch (\Exception $e) {
|
||||
Console::warning("Database not ready. Retrying connection ({$attempts})...");
|
||||
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
|
||||
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
|
||||
}
|
||||
sleep(DATABASE_RECONNECT_SLEEP);
|
||||
}
|
||||
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
function getInfluxDB(Registry &$register): InfluxDatabase
|
||||
{
|
||||
/** @var InfluxDB\Client $client */
|
||||
$client = $register->get('influxdb');
|
||||
$attempts = 0;
|
||||
$max = 10;
|
||||
$sleep = 1;
|
||||
|
||||
do { // check if telegraf database is ready
|
||||
try {
|
||||
$attempts++;
|
||||
$database = $client->selectDB('telegraf');
|
||||
if (in_array('telegraf', $client->listDatabases())) {
|
||||
break; // leave the do-while if successful
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
|
||||
if ($attempts >= $max) {
|
||||
throw new \Exception('InfluxDB database not ready yet');
|
||||
}
|
||||
sleep($sleep);
|
||||
}
|
||||
} while ($attempts < $max);
|
||||
return $database;
|
||||
}
|
||||
|
||||
$logError = function (Throwable $error, string $action = 'syncUsageStats') use ($register) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace("usage");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Usage stats log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
Console::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
};
|
||||
|
||||
$cli
|
||||
->task('usage')
|
||||
->desc('Schedules syncing data from influxdb to Appwrite console db')
|
||||
->action(function () use ($register, $logError) {
|
||||
Console::title('Usage Aggregation V1');
|
||||
Console::success(APP_NAME . ' usage aggregation process v1 has started');
|
||||
|
||||
$database = getDatabase($register, '_console');
|
||||
$influxDB = getInfluxDB($register);
|
||||
|
||||
$interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '30'); // 30 seconds (by default)
|
||||
$region = App::getEnv('region', 'default');
|
||||
$usage = new TimeSeries($region, $database, $influxDB, $logError);
|
||||
|
||||
Console::loop(function () use ($interval, $usage) {
|
||||
$now = date('d-m-Y H:i:s', time());
|
||||
Console::info("[{$now}] Aggregating Timeseries Usage data every {$interval} seconds");
|
||||
$loopStart = microtime(true);
|
||||
|
||||
$usage->collect();
|
||||
|
||||
$loopTook = microtime(true) - $loopStart;
|
||||
$now = date('d-m-Y H:i:s', time());
|
||||
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
|
||||
}, $interval);
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
$development = $this->getParam('development', false);
|
||||
$type = $this->getParam('type', 'general_server_error');
|
||||
$code = $this->getParam('code', 500);
|
||||
$errorID = $this->getParam('errorID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
|
||||
$message = $this->getParam('message', '');
|
||||
|
|
@ -13,25 +14,114 @@ $title = $this->getParam('title', '')
|
|||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $title; ?></title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-500.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink" />
|
||||
<link rel="preload" as="style" type="text/css" href="/fonts/main.css" />
|
||||
<link rel="stylesheet" href="/fonts/main.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="content-security-policy" content="">
|
||||
<title><?php echo $this->print($title, self::FILTER_ESCAPE); ?></title>
|
||||
|
||||
<style>
|
||||
@media(min-width:768px) {
|
||||
article.card {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container u-margin-block-start-24">
|
||||
<article class="card u-padding-16">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<h1 class="heading-level-4 u-trim-1">Error <?php echo $this->print($code, self::FILTER_ESCAPE); ?></h1>
|
||||
<p class="text"><?php echo $this->print($message, self::FILTER_ESCAPE); ?></p>
|
||||
<div class="u-flex u-flex-vertical u-gap-8">
|
||||
<p class="text">Type</p>
|
||||
<p><code class="inline-code"><?php echo $this->print($type, self::FILTER_ESCAPE); ?></code></p>
|
||||
</div>
|
||||
<?php if ($development) : ?>
|
||||
<h2 class="heading-level-5 u-trim-1">Error Trace</h2>
|
||||
<?php foreach ($trace as $log) : ?>
|
||||
<div class="table-with-scroll">
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
<?php foreach ($log as $key => $value) : ?>
|
||||
<tr>
|
||||
<td class="table-col" style="width: 120px"><?php echo $this->print($key, self::FILTER_ESCAPE); ?></td>
|
||||
<td class="table-col"><code class="grid-code u-max-height-200 u-overflow-x-auto u-overflow-y-auto">
|
||||
<?php if (is_array($value)) : ?>
|
||||
|
||||
<pre><?php echo $this->print(var_export($value, true), self::FILTER_ESCAPE); ?></pre>
|
||||
<?php else : ?>
|
||||
<pre><?php echo $this->print($value, self::FILTER_ESCAPE); ?></pre>
|
||||
<?php endif; ?>
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<section>
|
||||
<h1>Error <?php echo $code; ?></h1>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<!--section>
|
||||
<h1>Error <?php echo $this->print($code, self::FILTER_ESCAPE); ?></h1>
|
||||
|
||||
<p><?php echo $message; ?></p>
|
||||
<p><?php echo $this->print($message, self::FILTER_ESCAPE); ?></p>
|
||||
|
||||
<small>Error ID: <?php echo $errorID; ?></small>
|
||||
<small>Error ID: <?php echo $this->print($errorID, self::FILTER_ESCAPE); ?></small>
|
||||
|
||||
<?php if (!empty($projectURL)) : ?>
|
||||
<hr />
|
||||
|
||||
<p><a href="<?php echo $this->escape($projectURL); ?>" rel="noopener">Back to <?php echo $projectName; ?></a></p>
|
||||
<p><a href="<?php echo $this->print($projectURL, self::FILTER_ESCAPE); ?>" rel="noopener">Back to <?php echo $this->print($projectName, self::FILTER_ESCAPE); ?></a></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($development) : ?>
|
||||
|
|
@ -43,12 +133,12 @@ $title = $this->getParam('title', '')
|
|||
<table>
|
||||
<?php foreach ($log as $key => $value) : ?>
|
||||
<tr>
|
||||
<td style="width: 120px"><?php echo $key; ?></td>
|
||||
<td style="width: 120px"><?php echo $this->print($key, self::FILTER_ESCAPE); ?></td>
|
||||
<td>
|
||||
<?php if (is_array($value)) : ?>
|
||||
<?php var_dump($value); ?>
|
||||
<?php echo $this->print(var_export($value, true), self::FILTER_ESCAPE); ?>
|
||||
<?php else : ?>
|
||||
<?php echo $value; ?>
|
||||
<?php echo $this->print($value, self::FILTER_ESCAPE); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -59,7 +149,21 @@ $title = $this->getParam('title', '')
|
|||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</section>
|
||||
</section-->
|
||||
<script type="text/javascript">
|
||||
const app = (JSON.parse(localStorage.getItem('appwrite')) ?? {});
|
||||
const theme = app.theme ?? 'auto';
|
||||
if (theme === 'auto') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (darkThemeMq.matches) {
|
||||
document.body.setAttribute('class', `theme-dark`);
|
||||
} else {
|
||||
document.body.setAttribute('class', `theme-light`);
|
||||
}
|
||||
} else {
|
||||
document.body.setAttribute('class', `theme-${theme}`);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -15,7 +15,7 @@ $image = $this->getParam('image', '');
|
|||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:2.7
|
||||
image: traefik:2.9
|
||||
container_name: appwrite-traefik
|
||||
<<: *x-logging
|
||||
command:
|
||||
|
|
@ -88,9 +88,10 @@ services:
|
|||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
|
|
@ -110,34 +111,32 @@ services:
|
|||
- _APP_STORAGE_ANTIVIRUS
|
||||
- _APP_STORAGE_ANTIVIRUS_HOST
|
||||
- _APP_STORAGE_ANTIVIRUS_PORT
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CONTAINERS
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
|
|
@ -151,11 +150,21 @@ services:
|
|||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_SMS_PROVIDER
|
||||
- _APP_SMS_FROM
|
||||
- _APP_GRAPHQL_MAX_BATCH_SIZE
|
||||
- _APP_GRAPHQL_MAX_COMPLEXITY
|
||||
- _APP_GRAPHQL_MAX_DEPTH
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_VCS_GITHUB_WEBHOOK_SECRET
|
||||
- _APP_VCS_GITHUB_CLIENT_SECRET
|
||||
- _APP_VCS_GITHUB_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY
|
||||
|
||||
appwrite-realtime:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
|
|
@ -190,6 +199,8 @@ services:
|
|||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
|
|
@ -212,6 +223,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -238,6 +250,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
|
|
@ -266,36 +279,37 @@ services:
|
|||
- appwrite-certificates:/storage/certificates:rw
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
|
@ -315,6 +329,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -341,6 +356,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
|
|
@ -355,6 +371,15 @@ services:
|
|||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_DOMAIN
|
||||
|
||||
appwrite-worker-certificates:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
|
|
@ -372,13 +397,15 @@ services:
|
|||
- appwrite-certificates:/storage/certificates:rw
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
|
|
@ -399,9 +426,10 @@ services:
|
|||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- appwrite-executor
|
||||
- openruntimes-executor
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -413,69 +441,16 @@ services:
|
|||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_USAGE_STATS
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
appwrite-executor:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: executor
|
||||
<<: *x-logging
|
||||
container_name: appwrite-executor
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGINT
|
||||
networks:
|
||||
appwrite:
|
||||
runtimes:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- /tmp:/tmp:rw
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- appwrite
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_VERSION
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CONTAINERS
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- OPEN_RUNTIMES_NETWORK
|
||||
- _APP_DOCKER_HUB_USERNAME
|
||||
- _APP_DOCKER_HUB_PASSWORD
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
appwrite-worker-mails:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
|
|
@ -489,6 +464,7 @@ services:
|
|||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
|
|
@ -516,6 +492,7 @@ services:
|
|||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
|
|
@ -537,9 +514,11 @@ services:
|
|||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
|
|
@ -555,6 +534,7 @@ services:
|
|||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
|
||||
appwrite-usage:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
|
|
@ -569,6 +549,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
|
|
@ -597,10 +578,63 @@ services:
|
|||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
hostname: executor
|
||||
<<: *x-logging
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.3.5
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- /tmp:/tmp:rw
|
||||
environment:
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME
|
||||
- OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD
|
||||
- OPR_EXECUTOR_ENV=$_APP_ENV
|
||||
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES
|
||||
- OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
|
||||
- OPR_EXECUTOR_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER
|
||||
- OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
|
||||
- OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE
|
||||
- OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION
|
||||
- OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_BUCKET=$_APP_STORAGE_DO_SPACES_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_ACCESS_KEY=$_APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_SECRET=$_APP_STORAGE_BACKBLAZE_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_REGION=$_APP_STORAGE_BACKBLAZE_REGION
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_BUCKET=$_APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_ACCESS_KEY=$_APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_SECRET=$_APP_STORAGE_LINODE_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_REGION=$_APP_STORAGE_LINODE_REGION
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_BUCKET=$_APP_STORAGE_LINODE_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_ACCESS_KEY=$_APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_SECRET=$_APP_STORAGE_WASABI_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
|
|
@ -616,7 +650,7 @@ services:
|
|||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
command: 'mysqld --innodb-flush-method=fsync'
|
||||
command: 'mysqld --innodb-flush-method=fsync --max_connections=${_APP_CONNECTIONS_MAX}'
|
||||
|
||||
redis:
|
||||
image: redis:7.0.4-alpine
|
||||
|
|
@ -659,14 +693,17 @@ services:
|
|||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
environment:
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
|
||||
networks:
|
||||
gateway:
|
||||
name: gateway
|
||||
appwrite:
|
||||
name: appwrite
|
||||
runtimes:
|
||||
name: runtimes
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
|
|
@ -678,4 +715,3 @@ volumes:
|
|||
appwrite-builds:
|
||||
appwrite-influxdb:
|
||||
appwrite-config:
|
||||
appwrite-executor:
|
||||
|
|
|
|||
162
app/worker.php
Normal file
162
app/worker.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\CLI;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Queue\Adapter\Swoole;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Pools\Group;
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
global $register;
|
||||
|
||||
Server::setResource('register', fn() => $register);
|
||||
|
||||
Server::setResource('dbForConsole', function (Cache $cache, Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$database = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_console');
|
||||
|
||||
return $adapter;
|
||||
}, ['cache', 'register']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$database = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_' . $project->getInternalId());
|
||||
return $adapter;
|
||||
}, ['cache', 'register', 'message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
$adapters = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$adapters[] = $pools
|
||||
->get($value)
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
}
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('queueForFunctions', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
return new Func(
|
||||
$pools
|
||||
->get('queue')
|
||||
->pop()
|
||||
->getResource()
|
||||
);
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('log', fn() => new Log());
|
||||
|
||||
Server::setResource('logger', function ($register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('statsd', function ($register) {
|
||||
return $register->get('statsd');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('pools', function ($register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$connection = $pools->get('queue')->pop()->getResource();
|
||||
$workerNumber = swoole_cpu_num() * intval(App::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
if (empty(App::getEnv('QUEUE'))) {
|
||||
throw new Exception('Please configure "QUEUE" environment variable.');
|
||||
}
|
||||
|
||||
$adapter = new Swoole($connection, $workerNumber, App::getEnv('QUEUE'));
|
||||
$server = new Server($adapter);
|
||||
|
||||
$server
|
||||
->shutdown()
|
||||
->inject('pools')
|
||||
->action(function (Group $pools) {
|
||||
$pools->reclaim();
|
||||
});
|
||||
|
||||
$server
|
||||
->error()
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($error instanceof PDOException) {
|
||||
throw $error;
|
||||
}
|
||||
|
||||
if ($logger && ($error->getCode() >= 500 || $error->getCode() === 0)) {
|
||||
$log->setNamespace("appwrite-worker");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
$log->setAction('appwrite-queue-' . App::getEnv('QUEUE'));
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Usage stats log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\CLI\Console;
|
||||
|
|
@ -37,7 +36,7 @@ class AuditsV1 extends Worker
|
|||
$userName = $user->getAttribute('name', '');
|
||||
$userEmail = $user->getAttribute('email', '');
|
||||
|
||||
$dbForProject = $this->getProjectDB($project->getId());
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$audit = new Audit($dbForProject);
|
||||
$audit->log(
|
||||
userId: $user->getInternalId(),
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Swoole\Coroutine as Co;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
use Cron\CronExpression;
|
||||
use Executor\Executor;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Utopia\Database\Database;
|
||||
use Appwrite\Vcs\Comment;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
|
|
@ -43,12 +48,14 @@ class BuildsV1 extends Worker
|
|||
$project = new Document($this->args['project'] ?? []);
|
||||
$resource = new Document($this->args['resource'] ?? []);
|
||||
$deployment = new Document($this->args['deployment'] ?? []);
|
||||
$template = new Document($this->args['template'] ?? []);
|
||||
|
||||
switch ($type) {
|
||||
case BUILD_TYPE_DEPLOYMENT:
|
||||
case BUILD_TYPE_RETRY:
|
||||
Console::info('Creating build for deployment: ' . $deployment->getId());
|
||||
$this->buildDeployment($project, $resource, $deployment);
|
||||
$github = new GitHub($this->getCache());
|
||||
$this->buildDeployment($github, $project, $resource, $deployment, $template);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -57,9 +64,17 @@ class BuildsV1 extends Worker
|
|||
}
|
||||
}
|
||||
|
||||
protected function buildDeployment(Document $project, Document $function, Document $deployment)
|
||||
/**
|
||||
* @throws \Utopia\Database\Exception\Authorization
|
||||
* @throws \Utopia\Database\Exception\Structure
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildDeployment(GitHub $github, Document $project, Document $function, Document $deployment, Document $template)
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($project->getId());
|
||||
global $register;
|
||||
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $function->getId());
|
||||
if ($function->isEmpty()) {
|
||||
|
|
@ -71,6 +86,10 @@ class BuildsV1 extends Worker
|
|||
throw new Exception('Deployment not found', 404);
|
||||
}
|
||||
|
||||
if (empty($deployment->getAttribute('entrypoint', ''))) {
|
||||
throw new Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500);
|
||||
}
|
||||
|
||||
$runtimes = Config::getParam('runtimes', []);
|
||||
$key = $function->getAttribute('runtime');
|
||||
$runtime = isset($runtimes[$key]) ? $runtimes[$key] : null;
|
||||
|
|
@ -78,134 +97,380 @@ class BuildsV1 extends Worker
|
|||
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
$buildId = $deployment->getAttribute('buildId', '');
|
||||
// Realtime preparation
|
||||
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
|
||||
'functionId' => $function->getId(),
|
||||
'deploymentId' => $deployment->getId()
|
||||
]);
|
||||
|
||||
$startTime = DateTime::now();
|
||||
if (empty($buildId)) {
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
$buildId = $deployment->getAttribute('buildId', '');
|
||||
|
||||
$isNewBuild = empty($buildId);
|
||||
|
||||
if ($isNewBuild) {
|
||||
$buildId = ID::unique();
|
||||
$build = $dbForProject->createDocument('builds', new Document([
|
||||
'$id' => $buildId,
|
||||
'$permissions' => [],
|
||||
'startTime' => $startTime,
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'status' => 'processing',
|
||||
'outputPath' => '',
|
||||
'path' => '',
|
||||
'runtime' => $function->getAttribute('runtime'),
|
||||
'source' => $deployment->getAttribute('path'),
|
||||
'source' => $deployment->getAttribute('path', ''),
|
||||
'sourceType' => strtolower(App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)),
|
||||
'stdout' => '',
|
||||
'stderr' => '',
|
||||
'logs' => '',
|
||||
'endTime' => null,
|
||||
'duration' => 0
|
||||
'duration' => 0,
|
||||
'size' => 0
|
||||
]));
|
||||
$deployment->setAttribute('buildId', $buildId);
|
||||
|
||||
$deployment->setAttribute('buildId', $build->getId());
|
||||
$deployment->setAttribute('buildInternalId', $build->getInternalId());
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
} else {
|
||||
$build = $dbForProject->getDocument('builds', $buildId);
|
||||
}
|
||||
|
||||
/** Request the executor to build the code... */
|
||||
$build->setAttribute('status', 'building');
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
$source = $deployment->getAttribute('path', '');
|
||||
$installationId = $deployment->getAttribute('installationId', '');
|
||||
$providerRepositoryId = $deployment->getAttribute('providerRepositoryId', '');
|
||||
$providerCommitHash = $deployment->getAttribute('providerCommitHash', '');
|
||||
$isVcsEnabled = $providerRepositoryId ? true : false;
|
||||
$owner = '';
|
||||
$repositoryName = '';
|
||||
|
||||
/** Trigger Webhook */
|
||||
$deploymentModel = new Deployment();
|
||||
if ($isVcsEnabled) {
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
$providerInstallationId = $installation->getAttribute('providerInstallationId');
|
||||
$privateKey = App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = App::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
|
||||
$deploymentUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME);
|
||||
$deploymentUpdate
|
||||
->setProject($project)
|
||||
->setEvent('functions.[functionId].deployments.[deploymentId].update')
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('deploymentId', $deployment->getId())
|
||||
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())))
|
||||
->trigger();
|
||||
|
||||
/** Trigger Functions */
|
||||
$deploymentUpdate
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/** Trigger Realtime */
|
||||
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
|
||||
'functionId' => $function->getId(),
|
||||
'deploymentId' => $deployment->getId()
|
||||
]);
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $build,
|
||||
project: $project
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $build->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
|
||||
$source = $deployment->getAttribute('path');
|
||||
|
||||
$vars = array_reduce($function['vars'] ?? [], function (array $carry, Document $var) {
|
||||
$carry[$var->getAttribute('key')] = $var->getAttribute('value');
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
$baseImage = $runtime['image'];
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->executor->createRuntime(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
entrypoint: $deployment->getAttribute('entrypoint'),
|
||||
source: $source,
|
||||
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
|
||||
vars: $vars,
|
||||
runtime: $key,
|
||||
baseImage: $baseImage,
|
||||
workdir: '/usr/code',
|
||||
remove: true,
|
||||
commands: [
|
||||
'sh', '-c',
|
||||
'tar -zxf /tmp/code.tar.gz -C /usr/code && \
|
||||
cd /usr/local/src/ && ./build.sh'
|
||||
]
|
||||
if ($isNewBuild && $isVcsEnabled) {
|
||||
$tmpDirectory = '/tmp/builds/' . $buildId . '/code';
|
||||
$rootDirectory = $function->getAttribute('providerRootDirectory', '');
|
||||
$rootDirectory = \rtrim($rootDirectory, '/');
|
||||
$rootDirectory = \ltrim($rootDirectory, '.');
|
||||
$rootDirectory = \ltrim($rootDirectory, '/');
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
|
||||
$cloneOwner = $deployment->getAttribute('providerRepositoryOwner', $owner);
|
||||
$cloneRepository = $deployment->getAttribute('providerRepositoryName', $repositoryName);
|
||||
|
||||
$branchName = $deployment->getAttribute('providerBranch');
|
||||
$commitHash = $deployment->getAttribute('providerCommitHash', '');
|
||||
$gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $branchName, $tmpDirectory, $rootDirectory, $commitHash);
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
Console::execute('mkdir -p /tmp/builds/' . \escapeshellcmd($buildId), '', $stdout, $stderr);
|
||||
$exit = Console::execute($gitCloneCommand, '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to clone code repository: ' . $stderr);
|
||||
}
|
||||
|
||||
// Build from template
|
||||
$templateRepositoryName = $template->getAttribute('repositoryName', '');
|
||||
$templateOwnerName = $template->getAttribute('ownerName', '');
|
||||
$templateBranch = $template->getAttribute('branch', '');
|
||||
|
||||
$templateRootDirectory = $template->getAttribute('rootDirectory', '');
|
||||
$templateRootDirectory = \rtrim($templateRootDirectory, '/');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '.');
|
||||
$templateRootDirectory = \ltrim($templateRootDirectory, '/');
|
||||
|
||||
if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateBranch)) {
|
||||
// Clone template repo
|
||||
$tmpTemplateDirectory = '/tmp/builds/' . \escapeshellcmd($buildId) . '/template';
|
||||
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, $tmpTemplateDirectory, $templateRootDirectory);
|
||||
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to clone code repository: ' . $stderr);
|
||||
}
|
||||
|
||||
// Ensure directories
|
||||
Console::execute('mkdir -p ' . $tmpTemplateDirectory . '/' . $templateRootDirectory, '', $stdout, $stderr);
|
||||
Console::execute('mkdir -p ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
|
||||
|
||||
// Merge template into user repo
|
||||
Console::execute('cp -rfn ' . $tmpTemplateDirectory . '/' . $templateRootDirectory . '/* ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
|
||||
|
||||
// Commit and push
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . $tmpDirectory . ' && git add . && git commit -m "Create \'' . \escapeshellcmd($function->getAttribute('name', '')) . '\' function" && git push origin ' . \escapeshellcmd($branchName), '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to push code repository: ' . $stderr);
|
||||
}
|
||||
|
||||
$exit = Console::execute('cd ' . $tmpDirectory . ' && git rev-parse HEAD', '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to get vcs commit SHA: ' . $stderr);
|
||||
}
|
||||
|
||||
$providerCommitHash = \trim($stdout);
|
||||
$authorUrl = "https://github.com/$cloneOwner";
|
||||
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
|
||||
$deployment->setAttribute('providerCommitAuthorUrl', $authorUrl);
|
||||
$deployment->setAttribute('providerCommitAuthor', 'Appwrite');
|
||||
$deployment->setAttribute('providerCommitMessage', "Create '" . $function->getAttribute('name', '') . "' function");
|
||||
$deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash");
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
|
||||
/**
|
||||
* Send realtime Event
|
||||
*/
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $build,
|
||||
project: $project
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $build->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
|
||||
Console::execute('tar --exclude code.tar.gz -czf /tmp/builds/' . \escapeshellcmd($buildId) . '/code.tar.gz -C /tmp/builds/' . \escapeshellcmd($buildId) . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory) . ' .', '', $stdout, $stderr);
|
||||
|
||||
$deviceFunctions = $this->getFunctionsDevice($project->getId());
|
||||
|
||||
$fileName = 'code.tar.gz';
|
||||
$fileTmpName = '/tmp/builds/' . $buildId . '/code.tar.gz';
|
||||
|
||||
$path = $deviceFunctions->getPath($deployment->getId() . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
|
||||
$result = $deviceFunctions->move($fileTmpName, $path);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception("Unable to move file");
|
||||
}
|
||||
|
||||
Console::execute('rm -rf /tmp/builds/' . \escapeshellcmd($buildId), '', $stdout, $stderr);
|
||||
|
||||
$source = $path;
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
|
||||
|
||||
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
/** Request the executor to build the code... */
|
||||
$build->setAttribute('status', 'building');
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
/** Trigger Webhook */
|
||||
$deploymentModel = new Deployment();
|
||||
|
||||
$deploymentUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME);
|
||||
$deploymentUpdate
|
||||
->setProject($project)
|
||||
->setEvent('functions.[functionId].deployments.[deploymentId].update')
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('deploymentId', $deployment->getId())
|
||||
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())))
|
||||
->trigger();
|
||||
|
||||
/** Trigger Functions */
|
||||
$pools = $register->get('pools');
|
||||
$connection = $pools->get('queue')->pop();
|
||||
|
||||
$functions = new Func($connection->getResource());
|
||||
$functions
|
||||
->from($deploymentUpdate)
|
||||
->trigger();
|
||||
|
||||
$connection->reclaim();
|
||||
|
||||
/** Trigger Realtime */
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $build,
|
||||
project: $project
|
||||
);
|
||||
|
||||
/** Update the build document */
|
||||
$build->setAttribute('endTime', $response['endTime']);
|
||||
$build->setAttribute('duration', $response['duration']);
|
||||
$build->setAttribute('status', $response['status']);
|
||||
$build->setAttribute('outputPath', $response['outputPath']);
|
||||
$build->setAttribute('stderr', $response['stderr']);
|
||||
$build->setAttribute('stdout', $response['response']);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $build->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
|
||||
/* Also update the deployment buildTime */
|
||||
$deployment->setAttribute('buildTime', $response['duration']);
|
||||
$vars = [];
|
||||
|
||||
// Global vars
|
||||
$varsFromProject = $dbForProject->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($varsFromProject as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value') ?? '';
|
||||
}
|
||||
|
||||
// Function vars
|
||||
$vars = \array_merge($vars, array_reduce($function->getAttribute('vars', []), function (array $carry, Document $var) {
|
||||
$carry[$var->getAttribute('key')] = $var->getAttribute('value');
|
||||
return $carry;
|
||||
}, []));
|
||||
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
]);
|
||||
|
||||
$command = $deployment->getAttribute('commands', '');
|
||||
$command = \str_replace('"', '\\"', $command);
|
||||
|
||||
$response = null;
|
||||
|
||||
$err = null;
|
||||
|
||||
// TODO: Remove run() wrapper when switching to new utopia queue. That should be done on Swoole adapter in the libary
|
||||
Co\run(function () use ($project, $deployment, &$response, $source, $function, $runtime, $vars, $command, &$build, $dbForProject, $allEvents, &$err) {
|
||||
Co::join([
|
||||
Co\go(function () use (&$response, $project, $deployment, $source, $function, $runtime, $vars, $command, &$err) {
|
||||
try {
|
||||
$response = $this->executor->createRuntime(
|
||||
deploymentId: $deployment->getId(),
|
||||
projectId: $project->getId(),
|
||||
source: $source,
|
||||
image: $runtime['image'],
|
||||
version: $function->getAttribute('version'),
|
||||
remove: true,
|
||||
entrypoint: $deployment->getAttribute('entrypoint'),
|
||||
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
|
||||
variables: $vars,
|
||||
command: 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . $command . '"'
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
$err = $error;
|
||||
}
|
||||
}),
|
||||
Co\go(function () use ($project, $deployment, &$response, &$build, $dbForProject, $allEvents, &$err) {
|
||||
try {
|
||||
$this->executor->getLogs(
|
||||
deploymentId: $deployment->getId(),
|
||||
projectId: $project->getId(),
|
||||
callback: function ($logs) use (&$response, &$build, $dbForProject, $allEvents, $project) {
|
||||
if ($response === null) {
|
||||
$build = $dbForProject->getDocument('builds', $build->getId());
|
||||
|
||||
if ($build->isEmpty()) {
|
||||
throw new Exception('Build not found', 404);
|
||||
}
|
||||
|
||||
$build = $build->setAttribute('logs', $build->getAttribute('logs', '') . $logs);
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
|
||||
|
||||
/**
|
||||
* Send realtime Event
|
||||
*/
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $build,
|
||||
project: $project
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $build->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
if (empty($err)) {
|
||||
$err = $error;
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
if ($err) {
|
||||
throw $err;
|
||||
}
|
||||
|
||||
$endTime = DateTime::now();
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
/** Update the build document */
|
||||
$build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp($response['startTime'])));
|
||||
$build->setAttribute('endTime', $endTime);
|
||||
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
|
||||
$build->setAttribute('status', 'ready');
|
||||
$build->setAttribute('path', $response['path']);
|
||||
$build->setAttribute('size', $response['size']);
|
||||
$build->setAttribute('logs', $response['output']);
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
Console::success("Build id: $buildId created");
|
||||
|
||||
/** Set auto deploy */
|
||||
if ($deployment->getAttribute('activate') === true) {
|
||||
$function->setAttribute('deploymentInternalId', $deployment->getInternalId());
|
||||
$function->setAttribute('deployment', $deployment->getId());
|
||||
$function->setAttribute('live', true);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
}
|
||||
|
||||
/** Update function schedule */
|
||||
$schedule = $function->getAttribute('schedule', '');
|
||||
$cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null;
|
||||
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? DateTime::format($cron->getNextRunDate()) : null;
|
||||
$function->setAttribute('scheduleNext', $next);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
// Inform scheduler if function is still active
|
||||
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
|
||||
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
} catch (\Throwable $th) {
|
||||
$endTime = DateTime::now();
|
||||
$interval = (new \DateTime($endTime))->diff(new \DateTime($startTime));
|
||||
$durationEnd = \microtime(true);
|
||||
$build->setAttribute('endTime', $endTime);
|
||||
$build->setAttribute('duration', $interval->format('%s') + 0);
|
||||
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
|
||||
$build->setAttribute('status', 'failed');
|
||||
$build->setAttribute('stderr', $th->getMessage());
|
||||
$build->setAttribute('logs', $th->getMessage());
|
||||
Console::error($th->getMessage());
|
||||
Console::error($th->getFile() . ':' . $th->getLine());
|
||||
Console::error($th->getTraceAsString());
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
} finally {
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
|
||||
|
|
@ -213,7 +478,7 @@ class BuildsV1 extends Worker
|
|||
* Send realtime Event
|
||||
*/
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $build,
|
||||
project: $project
|
||||
|
|
@ -227,11 +492,11 @@ class BuildsV1 extends Worker
|
|||
);
|
||||
|
||||
/** Update usage stats */
|
||||
global $register;
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
|
||||
$statsd = $register->get('statsd');
|
||||
$usage = new Stats($statsd);
|
||||
$usage
|
||||
->setParam('projectInternalId', $project->getInternalId())
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('builds.{scope}.compute', 1)
|
||||
|
|
@ -244,6 +509,76 @@ class BuildsV1 extends Worker
|
|||
}
|
||||
}
|
||||
|
||||
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
|
||||
{
|
||||
if ($function->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$commentId = $deployment->getAttribute('providerCommentId', '');
|
||||
|
||||
if (!empty($providerCommitHash)) {
|
||||
$message = match ($status) {
|
||||
'ready' => 'Build succeeded.',
|
||||
'failed' => 'Build failed.',
|
||||
'processing' => 'Building...',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$state = match ($status) {
|
||||
'ready' => 'success',
|
||||
'failed' => 'failure',
|
||||
'processing' => 'pending',
|
||||
default => $status
|
||||
};
|
||||
|
||||
$functionName = $function->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
|
||||
$name = "{$functionName} ({$projectName})";
|
||||
|
||||
$protocol = App::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = App::getEnv('_APP_DOMAIN');
|
||||
$functionId = $function->getId();
|
||||
$projectId = $project->getId();
|
||||
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$projectId/functions/function-$functionId";
|
||||
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
|
||||
}
|
||||
|
||||
if (!empty($commentId)) {
|
||||
$retries = 0;
|
||||
|
||||
while (true) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForConsole->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $commentId
|
||||
]));
|
||||
break;
|
||||
} catch (Exception $err) {
|
||||
if ($retries >= 9) {
|
||||
throw $err;
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
|
||||
$comment->addBuild($project, $function, $status, $deployment->getId(), ['type' => 'logs']);
|
||||
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
|
||||
} finally {
|
||||
$dbForConsole->deleteDocument('vcsCommentLocks', $commentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Network\Validator\CNAME;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Template\Template;
|
||||
use Utopia\App;
|
||||
|
|
@ -47,7 +49,7 @@ class CertificatesV1 extends Worker
|
|||
* 4. Validate security email. Cannot be empty, required by LetsEncrypt
|
||||
* 5. Validate renew date with certificate file, unless requested to skip by parameter
|
||||
* 6. Issue a certificate using certbot CLI
|
||||
* 7. Update 'log' attribute on certificate document with Certbot message
|
||||
* 7. Update 'logs' attribute on certificate document with Certbot message
|
||||
* 8. Create storage folder for certificate, if not ready already
|
||||
* 9. Move certificates from Certbot location to our Storage
|
||||
* 10. Create/Update our Storage with new Traefik config with new certificate paths
|
||||
|
|
@ -57,7 +59,7 @@ class CertificatesV1 extends Worker
|
|||
* If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker
|
||||
*
|
||||
* If code stops with expected error:
|
||||
* 1. 'log' attribute on document is updated with error message
|
||||
* 1. 'logs' attribute on document is updated with error message
|
||||
* 2. 'attempts' amount is increased
|
||||
* 3. Console log is shown
|
||||
* 4. Email is sent to security email
|
||||
|
|
@ -85,6 +87,8 @@ class CertificatesV1 extends Worker
|
|||
$certificate->setAttribute('domain', $domain->get());
|
||||
}
|
||||
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
// Email for alerts is required by LetsEncrypt
|
||||
$email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
|
||||
|
|
@ -111,11 +115,8 @@ class CertificatesV1 extends Worker
|
|||
$letsEncryptData = $this->issueCertificate($folder, $domain->get(), $email);
|
||||
|
||||
// Command succeeded, store all data into document
|
||||
// We store stderr too, because it may include warnings
|
||||
$certificate->setAttribute('log', \json_encode([
|
||||
'stdout' => $letsEncryptData['stdout'],
|
||||
'stderr' => $letsEncryptData['stderr'],
|
||||
]));
|
||||
$logs = 'Certificate successfully generated.';
|
||||
$certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB
|
||||
|
||||
// Give certificates to Traefik
|
||||
$this->applyCertificateFiles($folder, $domain->get(), $letsEncryptData);
|
||||
|
|
@ -124,9 +125,13 @@ class CertificatesV1 extends Worker
|
|||
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
|
||||
$certificate->setAttribute('attempts', 0);
|
||||
$certificate->setAttribute('issueDate', DateTime::now());
|
||||
|
||||
$success = true;
|
||||
} catch (Throwable $e) {
|
||||
$logs = $e->getMessage();
|
||||
|
||||
// Set exception as log in certificate document
|
||||
$certificate->setAttribute('log', $e->getMessage());
|
||||
$certificate->setAttribute('logs', \mb_strcut($logs, 0, 1000000));// Limit to 1MB
|
||||
|
||||
// Increase attempts count
|
||||
$attempts = $certificate->getAttribute('attempts', 0) + 1;
|
||||
|
|
@ -142,7 +147,7 @@ class CertificatesV1 extends Worker
|
|||
$certificate->setAttribute('updated', DateTime::now());
|
||||
|
||||
// Save all changes we made to certificate document into database
|
||||
$this->saveCertificateDocument($domain->get(), $certificate);
|
||||
$this->saveCertificateDocument($domain->get(), $certificate, $success);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +160,11 @@ class CertificatesV1 extends Worker
|
|||
*
|
||||
* @param string $domain Domain name that certificate is for
|
||||
* @param Document $certificate Certificate document that we need to save
|
||||
* @param bool $success Was certificate generation successful?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function saveCertificateDocument(string $domain, Document $certificate): void
|
||||
private function saveCertificateDocument(string $domain, Document $certificate, bool $success): void
|
||||
{
|
||||
// Check if update or insert required
|
||||
$certificateDocument = $this->dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]);
|
||||
|
|
@ -172,7 +178,7 @@ class CertificatesV1 extends Worker
|
|||
}
|
||||
|
||||
$certificateId = $certificate->getId();
|
||||
$this->updateDomainDocuments($certificateId, $domain);
|
||||
$this->updateDomainDocuments($certificateId, $domain, $success);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,11 +191,6 @@ class CertificatesV1 extends Worker
|
|||
$envDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
if (!empty($envDomain) && $envDomain !== 'localhost') {
|
||||
return $envDomain;
|
||||
} else {
|
||||
$domainDocument = $this->dbForConsole->findOne('domains', [Query::orderAsc('_id')]);
|
||||
if ($domainDocument) {
|
||||
return $domainDocument->getAttribute('domain');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -280,7 +281,7 @@ class CertificatesV1 extends Worker
|
|||
$stderr = '';
|
||||
|
||||
$staging = (App::isProduction()) ? '' : ' --dry-run';
|
||||
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
|
||||
$exit = Console::execute("certbot certonly -v --webroot --noninteractive --agree-tos{$staging}"
|
||||
. " --email " . $email
|
||||
. " --cert-name " . $folder
|
||||
. " -w " . APP_STORAGE_CERTIFICATES
|
||||
|
|
@ -421,25 +422,64 @@ class CertificatesV1 extends Worker
|
|||
*
|
||||
* @param string $certificateId ID of a new or updated certificate document
|
||||
* @param string $domain Domain that is affected by new certificate
|
||||
* @param bool $success Was certificate generation successful?
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function updateDomainDocuments(string $certificateId, string $domain): void
|
||||
private function updateDomainDocuments(string $certificateId, string $domain, bool $success): void
|
||||
{
|
||||
$domains = $this->dbForConsole->find('domains', [
|
||||
$rule = $this->dbForConsole->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
Query::limit(1000),
|
||||
]);
|
||||
|
||||
foreach ($domains as $domainDocument) {
|
||||
$domainDocument->setAttribute('updated', DateTime::now());
|
||||
$domainDocument->setAttribute('certificateId', $certificateId);
|
||||
if ($rule !== false && !$rule->isEmpty()) {
|
||||
$rule->setAttribute('certificateId', $certificateId);
|
||||
$rule->setAttribute('status', $success ? 'verified' : 'unverified');
|
||||
$this->dbForConsole->updateDocument('rules', $rule->getId(), $rule);
|
||||
|
||||
$this->dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument);
|
||||
$projectId = $rule->getAttribute('projectId');
|
||||
$project = $this->dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($domainDocument->getAttribute('projectId')) {
|
||||
$this->dbForConsole->deleteCachedDocument('projects', $domainDocument->getAttribute('projectId'));
|
||||
}
|
||||
/** Trigger Webhook */
|
||||
$ruleModel = new Rule();
|
||||
$ruleUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME);
|
||||
$ruleUpdate
|
||||
->setProject($project)
|
||||
->setEvent('rules.[ruleId].update')
|
||||
->setParam('ruleId', $rule->getId())
|
||||
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
|
||||
->trigger();
|
||||
|
||||
/** Trigger Functions */
|
||||
$ruleUpdate
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/** Trigger realtime event */
|
||||
$allEvents = Event::generateEvents('rules.[ruleId].update', [
|
||||
'ruleId' => $rule->getId(),
|
||||
]);
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $rule,
|
||||
project: $project
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: $project->getId(),
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Appwrite\Resque\Worker;
|
|||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Exception as DatabaseException;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
|
|
@ -29,25 +29,25 @@ class DatabaseV1 extends Worker
|
|||
$database = new Document($this->args['database'] ?? []);
|
||||
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception('Missing collection');
|
||||
throw new DatabaseException('Missing collection');
|
||||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception('Missing document');
|
||||
throw new DatabaseException('Missing document');
|
||||
}
|
||||
|
||||
switch (strval($type)) {
|
||||
case DATABASE_TYPE_CREATE_ATTRIBUTE:
|
||||
$this->createAttribute($database, $collection, $document, $project->getId());
|
||||
$this->createAttribute($database, $collection, $document, $project);
|
||||
break;
|
||||
case DATABASE_TYPE_DELETE_ATTRIBUTE:
|
||||
$this->deleteAttribute($database, $collection, $document, $project->getId());
|
||||
$this->deleteAttribute($database, $collection, $document, $project);
|
||||
break;
|
||||
case DATABASE_TYPE_CREATE_INDEX:
|
||||
$this->createIndex($database, $collection, $document, $project->getId());
|
||||
$this->createIndex($database, $collection, $document, $project);
|
||||
break;
|
||||
case DATABASE_TYPE_DELETE_INDEX:
|
||||
$this->deleteIndex($database, $collection, $document, $project->getId());
|
||||
$this->deleteIndex($database, $collection, $document, $project);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -64,12 +64,13 @@ class DatabaseV1 extends Worker
|
|||
* @param Document $database
|
||||
* @param Document $collection
|
||||
* @param Document $attribute
|
||||
* @param string $projectId
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function createAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
|
||||
protected function createAttribute(Document $database, Document $collection, Document $attribute, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [
|
||||
'databaseId' => $database->getId(),
|
||||
|
|
@ -100,7 +101,7 @@ class DatabaseV1 extends Worker
|
|||
case Database::VAR_RELATIONSHIP:
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new Exception('Collection not found');
|
||||
throw new DatabaseException('Collection not found');
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -114,7 +115,7 @@ class DatabaseV1 extends Worker
|
|||
onDelete: $options['onDelete'],
|
||||
)
|
||||
) {
|
||||
throw new Exception('Failed to create Attribute');
|
||||
throw new DatabaseException('Failed to create Attribute');
|
||||
}
|
||||
|
||||
if ($options['twoWay']) {
|
||||
|
|
@ -129,13 +130,28 @@ class DatabaseV1 extends Worker
|
|||
}
|
||||
|
||||
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
|
||||
} catch (\Exception $e) {
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
|
||||
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'failed'));
|
||||
if ($e instanceof DatabaseException) {
|
||||
$attribute->setAttribute('error', $e->getMessage());
|
||||
if (isset($relatedAttribute)) {
|
||||
$relatedAttribute->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$attribute->getId(),
|
||||
$attribute->setAttribute('status', 'failed')
|
||||
);
|
||||
|
||||
if (isset($relatedAttribute)) {
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$relatedAttribute->getId(),
|
||||
$relatedAttribute->setAttribute('status', 'failed')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
$target = Realtime::fromPayload(
|
||||
|
|
@ -170,13 +186,14 @@ class DatabaseV1 extends Worker
|
|||
* @param Document $database
|
||||
* @param Document $collection
|
||||
* @param Document $attribute
|
||||
* @param string $projectId
|
||||
* @param Document $project
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function deleteAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
|
||||
protected function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [
|
||||
'databaseId' => $database->getId(),
|
||||
|
|
@ -200,21 +217,21 @@ class DatabaseV1 extends Worker
|
|||
|
||||
try {
|
||||
if ($status !== 'failed') {
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
if ($options['twoWay']) {
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
throw new DatabaseException('Collection not found');
|
||||
}
|
||||
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
|
||||
throw new Exception('Failed to delete Relationship');
|
||||
throw new DatabaseException('Failed to delete Relationship');
|
||||
}
|
||||
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
throw new Exception('Failed to delete Attribute');
|
||||
throw new DatabaseException('Failed to delete Attribute');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,9 +240,27 @@ class DatabaseV1 extends Worker
|
|||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
|
||||
} catch (\Exception $e) {
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$attribute->setAttribute('error', $e->getMessage());
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$relatedAttribute->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$attribute->getId(),
|
||||
$attribute->setAttribute('status', 'stuck')
|
||||
);
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$relatedAttribute->getId(),
|
||||
$relatedAttribute->setAttribute('status', 'stuck')
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
|
|
@ -275,8 +310,7 @@ class DatabaseV1 extends Worker
|
|||
$index
|
||||
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN)
|
||||
;
|
||||
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN);
|
||||
|
||||
// Check if an index exists with the same attributes and orders
|
||||
$exists = false;
|
||||
|
|
@ -292,7 +326,7 @@ class DatabaseV1 extends Worker
|
|||
}
|
||||
|
||||
if ($exists) { // Delete the duplicate if created, else update in db
|
||||
$this->deleteIndex($database, $collection, $index, $projectId);
|
||||
$this->deleteIndex($database, $collection, $index, $project);
|
||||
} else {
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index);
|
||||
}
|
||||
|
|
@ -313,12 +347,14 @@ class DatabaseV1 extends Worker
|
|||
* @param Document $database
|
||||
* @param Document $collection
|
||||
* @param Document $index
|
||||
* @param string $projectId
|
||||
* @param Document $project
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function createIndex(Document $database, Document $collection, Document $index, string $projectId): void
|
||||
protected function createIndex(Document $database, Document $collection, Document $index, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [
|
||||
'databaseId' => $database->getId(),
|
||||
|
|
@ -335,12 +371,20 @@ class DatabaseV1 extends Worker
|
|||
|
||||
try {
|
||||
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
|
||||
throw new Exception('Failed to create Index');
|
||||
throw new DatabaseException('Failed to create Index');
|
||||
}
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
|
||||
} catch (\Exception $e) {
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$index->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
$dbForProject->updateDocument(
|
||||
'indexes',
|
||||
$index->getId(),
|
||||
$index->setAttribute('status', 'failed')
|
||||
);
|
||||
} finally {
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
|
|
@ -370,12 +414,13 @@ class DatabaseV1 extends Worker
|
|||
* @param Document $database
|
||||
* @param Document $collection
|
||||
* @param Document $index
|
||||
* @param string $projectId
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteIndex(Document $database, Document $collection, Document $index, string $projectId): void
|
||||
protected function deleteIndex(Document $database, Document $collection, Document $index, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [
|
||||
'databaseId' => $database->getId(),
|
||||
|
|
@ -388,12 +433,20 @@ class DatabaseV1 extends Worker
|
|||
|
||||
try {
|
||||
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
throw new Exception('Failed to delete index');
|
||||
throw new DatabaseException('Failed to delete index');
|
||||
}
|
||||
$dbForProject->deleteDocument('indexes', $index->getId());
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
|
||||
} catch (\Exception $e) {
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$index->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
$dbForProject->updateDocument(
|
||||
'indexes',
|
||||
$index->getId(),
|
||||
$index->setAttribute('status', 'stuck')
|
||||
);
|
||||
} finally {
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
|
|
@ -8,7 +9,6 @@ use Utopia\Database\Database;
|
|||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
use Utopia\CLI\Console;
|
||||
|
|
@ -41,32 +41,41 @@ class DeletesV1 extends Worker
|
|||
|
||||
switch ($document->getCollection()) {
|
||||
case DELETE_TYPE_DATABASES:
|
||||
$this->deleteDatabase($document, $project->getId());
|
||||
$this->deleteDatabase($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_COLLECTIONS:
|
||||
$this->deleteCollection($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_PROJECTS:
|
||||
$this->deleteProject($document);
|
||||
break;
|
||||
case DELETE_TYPE_FUNCTIONS:
|
||||
$this->deleteFunction($document, $project->getId());
|
||||
$this->deleteFunction($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_DEPLOYMENTS:
|
||||
$this->deleteDeployment($document, $project->getId());
|
||||
$this->deleteDeployment($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_USERS:
|
||||
$this->deleteUser($document, $project->getId());
|
||||
$this->deleteUser($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_TEAMS:
|
||||
$this->deleteMemberships($document, $project->getId());
|
||||
$this->deleteMemberships($document, $project);
|
||||
if ($project->getId() === 'console') {
|
||||
$this->deleteProjectsByTeam($document);
|
||||
}
|
||||
break;
|
||||
case DELETE_TYPE_BUCKETS:
|
||||
$this->deleteBucket($document, $project->getId());
|
||||
$this->deleteBucket($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_INSTALLATIONS:
|
||||
$this->deleteInstallation($document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_RULES:
|
||||
$this->deleteRule($document, $project);
|
||||
break;
|
||||
default:
|
||||
if (\str_starts_with($document->getCollection(), 'database_')) {
|
||||
$this->deleteCollection($document, $project->getId());
|
||||
$this->deleteCollection($document, $project);
|
||||
break;
|
||||
}
|
||||
Console::error('No lazy delete operation available for document of type: ' . $document->getCollection());
|
||||
|
|
@ -87,7 +96,7 @@ class DeletesV1 extends Worker
|
|||
$document = new Document($this->args['document'] ?? []);
|
||||
|
||||
if (!$document->isEmpty()) {
|
||||
$this->deleteAuditLogsByResource('document/' . $document->getId(), $project->getId());
|
||||
$this->deleteAuditLogsByResource('document/' . $document->getId(), $project);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -104,11 +113,6 @@ class DeletesV1 extends Worker
|
|||
$this->deleteExpiredSessions();
|
||||
break;
|
||||
|
||||
case DELETE_TYPE_CERTIFICATES:
|
||||
$document = new Document($this->args['document']);
|
||||
$this->deleteCertificates($document);
|
||||
break;
|
||||
|
||||
case DELETE_TYPE_USAGE:
|
||||
$this->deleteUsageStats($this->args['hourlyUsageRetentionDatetime']);
|
||||
break;
|
||||
|
|
@ -119,6 +123,9 @@ class DeletesV1 extends Worker
|
|||
case DELETE_TYPE_CACHE_BY_TIMESTAMP:
|
||||
$this->deleteCacheByDate($this->args['datetime']);
|
||||
break;
|
||||
case DELETE_TYPE_SCHEDULES:
|
||||
$this->deleteSchedules($this->args['datetime']);
|
||||
break;
|
||||
default:
|
||||
Console::error('No delete operation for type: ' . $type);
|
||||
break;
|
||||
|
|
@ -129,6 +136,39 @@ class DeletesV1 extends Worker
|
|||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function deleteSchedules(string $datetime): void
|
||||
{
|
||||
$this->listByGroup(
|
||||
'schedules',
|
||||
[
|
||||
Query::equal('region', [App::getEnv('_APP_REGION', 'default')]),
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::lessThanEqual('resourceUpdatedAt', $datetime),
|
||||
Query::equal('active', [false]),
|
||||
],
|
||||
$this->getConsoleDB(),
|
||||
function (Document $document) {
|
||||
$project = $this->getConsoleDB()->getDocument('projects', $document->getAttribute('projectId'));
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
$this->getConsoleDB()->deleteDocument('schedules', $document->getId());
|
||||
Console::success('Deleted schedule for deleted project ' . $document->getAttribute('projectId'));
|
||||
return;
|
||||
}
|
||||
|
||||
$function = $this->getProjectDB($project)->getDocument('functions', $document->getAttribute('resourceId'));
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
$this->getConsoleDB()->deleteDocument('schedules', $document->getId());
|
||||
Console::success('Deleted schedule for function ' . $document->getAttribute('resourceId'));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document $project
|
||||
* @param string $resource
|
||||
|
|
@ -137,30 +177,28 @@ class DeletesV1 extends Worker
|
|||
protected function deleteCacheByResource(Document $project, string $resource): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$document = $dbForProject->findOne('cache', [Query::equal('resource', [$resource])]);
|
||||
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
|
||||
);
|
||||
if ($document) {
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
|
||||
);
|
||||
|
||||
$query = [
|
||||
Query::equal('resource', [$resource])
|
||||
];
|
||||
$this->deleteById(
|
||||
$document,
|
||||
$dbForProject,
|
||||
function ($document) use ($cache, $projectId) {
|
||||
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
|
||||
|
||||
$this->deleteByGroup(
|
||||
'cache',
|
||||
$query,
|
||||
$dbForProject,
|
||||
function (Document $document) use ($cache, $projectId) {
|
||||
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
|
||||
|
||||
if ($cache->purge($document->getId())) {
|
||||
Console::success('Deleting cache file: ' . $path);
|
||||
} else {
|
||||
Console::error('Failed to delete cache file: ' . $path);
|
||||
if ($cache->purge($document->getId())) {
|
||||
Console::success('Deleting cache file: ' . $path);
|
||||
} else {
|
||||
Console::error('Failed to delete cache file: ' . $path);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -169,9 +207,9 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
protected function deleteCacheByDate(string $datetime): void
|
||||
{
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
|
||||
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($datetime) {
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
|
||||
);
|
||||
|
|
@ -197,38 +235,39 @@ class DeletesV1 extends Worker
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Document $document database document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteDatabase(Document $document, string $projectId): void
|
||||
protected function deleteDatabase(Document $document, Document $project): void
|
||||
{
|
||||
$databaseId = $document->getId();
|
||||
$projectId = $project->getId();
|
||||
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$this->deleteByGroup('database_' . $document->getInternalId(), [], $dbForProject, function ($document) use ($projectId) {
|
||||
$this->deleteCollection($document, $projectId);
|
||||
$this->deleteByGroup('database_' . $document->getInternalId(), [], $dbForProject, function ($document) use ($project) {
|
||||
$this->deleteCollection($document, $project);
|
||||
});
|
||||
|
||||
$dbForProject->deleteCollection('database_' . $document->getInternalId());
|
||||
|
||||
$this->deleteAuditLogsByResource('database/' . $databaseId, $projectId);
|
||||
$this->deleteAuditLogsByResource('database/' . $databaseId, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document $document teams document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteCollection(Document $document, string $projectId): void
|
||||
protected function deleteCollection(Document $document, Document $project): void
|
||||
{
|
||||
$collectionId = $document->getId();
|
||||
$collectionInternalId = $document->getInternalId();
|
||||
$databaseId = $document->getAttribute('databaseId');
|
||||
$databaseInternalId = $document->getAttribute('databaseInternalId');
|
||||
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$relationships = \array_filter(
|
||||
$document->getAttribute('attributes'),
|
||||
|
|
@ -248,16 +287,16 @@ class DeletesV1 extends Worker
|
|||
$dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $document->getInternalId());
|
||||
|
||||
$this->deleteByGroup('attributes', [
|
||||
Query::equal('databaseId', [$databaseId]),
|
||||
Query::equal('collectionId', [$collectionId])
|
||||
Query::equal('databaseInternalId', [$databaseInternalId]),
|
||||
Query::equal('collectionInternalId', [$collectionInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
$this->deleteByGroup('indexes', [
|
||||
Query::equal('databaseId', [$databaseId]),
|
||||
Query::equal('collectionId', [$collectionId])
|
||||
Query::equal('databaseInternalId', [$databaseInternalId]),
|
||||
Query::equal('collectionInternalId', [$collectionInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
$this->deleteAuditLogsByResource('database/' . $databaseId . '/collection/' . $collectionId, $projectId);
|
||||
$this->deleteAuditLogsByResource('database/' . $databaseId . '/collection/' . $collectionId, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -266,8 +305,9 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
protected function deleteUsageStats(string $hourlyUsageRetentionDatetime)
|
||||
{
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($hourlyUsageRetentionDatetime) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($hourlyUsageRetentionDatetime) {
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
// Delete Usage stats
|
||||
$this->deleteByGroup('stats', [
|
||||
Query::lessThan('time', $hourlyUsageRetentionDatetime),
|
||||
Query::equal('period', ['1h']),
|
||||
|
|
@ -277,17 +317,25 @@ class DeletesV1 extends Worker
|
|||
|
||||
/**
|
||||
* @param Document $document teams document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteMemberships(Document $document, string $projectId): void
|
||||
protected function deleteMemberships(Document $document, Document $project): void
|
||||
{
|
||||
$teamId = $document->getAttribute('teamId', '');
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$teamInternalId = $document->getInternalId();
|
||||
|
||||
// Delete Memberships
|
||||
$this->deleteByGroup('memberships', [
|
||||
Query::equal('teamId', [$teamId])
|
||||
], $this->getProjectDB($projectId));
|
||||
$this->deleteByGroup(
|
||||
'memberships',
|
||||
[
|
||||
Query::equal('teamInternalId', [$teamInternalId])
|
||||
],
|
||||
$dbForProject,
|
||||
function (Document $membership) use ($dbForProject) {
|
||||
$userId = $membership->getAttribute('userId');
|
||||
$dbForProject->deleteCachedDocument('users', $userId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -305,6 +353,7 @@ class DeletesV1 extends Worker
|
|||
|
||||
foreach ($projects as $project) {
|
||||
$this->deleteProject($project);
|
||||
$dbForConsole->deleteDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +378,7 @@ class DeletesV1 extends Worker
|
|||
}
|
||||
|
||||
// Delete project tables
|
||||
$dbForProject = $this->getProjectDB($projectId, $document);
|
||||
$dbForProject = $this->getProjectDB($document);
|
||||
|
||||
while (true) {
|
||||
$collections = $dbForProject->listCollections();
|
||||
|
|
@ -385,45 +434,49 @@ class DeletesV1 extends Worker
|
|||
|
||||
/**
|
||||
* @param Document $document user document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteUser(Document $document, string $projectId): void
|
||||
protected function deleteUser(Document $document, Document $project): void
|
||||
{
|
||||
$userId = $document->getId();
|
||||
$userInternalId = $document->getInternalId();
|
||||
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
// Delete all sessions of this user from the sessions table and update the sessions field of the user record
|
||||
$this->deleteByGroup('sessions', [
|
||||
Query::equal('userId', [$userId])
|
||||
], $this->getProjectDB($projectId));
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
$this->getProjectDB($projectId)->deleteCachedDocument('users', $userId);
|
||||
$dbForProject->deleteCachedDocument('users', $userId);
|
||||
|
||||
// Delete Memberships and decrement team membership counts
|
||||
$this->deleteByGroup('memberships', [
|
||||
Query::equal('userId', [$userId])
|
||||
], $this->getProjectDB($projectId), function (Document $document) use ($projectId) {
|
||||
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject, function (Document $document) use ($dbForProject) {
|
||||
if ($document->getAttribute('confirm')) { // Count only confirmed members
|
||||
$teamId = $document->getAttribute('teamId');
|
||||
$team = $this->getProjectDB($projectId)->getDocument('teams', $teamId);
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
if (!$team->isEmpty()) {
|
||||
$team = $this
|
||||
->getProjectDB($projectId)
|
||||
->updateDocument(
|
||||
'teams',
|
||||
$teamId,
|
||||
// Ensure that total >= 0
|
||||
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0))
|
||||
);
|
||||
$team = $dbForProject->updateDocument(
|
||||
'teams',
|
||||
$teamId,
|
||||
// Ensure that total >= 0
|
||||
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Delete tokens
|
||||
$this->deleteByGroup('tokens', [
|
||||
Query::equal('userId', [$userId])
|
||||
], $this->getProjectDB($projectId));
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
// Delete identities
|
||||
$this->deleteByGroup('identities', [
|
||||
Query::equal('userInternalId', [$userInternalId])
|
||||
], $dbForProject);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -432,8 +485,8 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
protected function deleteExecutionLogs(string $datetime): void
|
||||
{
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
// Delete Executions
|
||||
$this->deleteByGroup('executions', [
|
||||
Query::lessThan('$createdAt', $datetime)
|
||||
|
|
@ -445,10 +498,10 @@ class DeletesV1 extends Worker
|
|||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($consoleDB) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($consoleDB) {
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$project = $consoleDB->getDocument('projects', $projectId);
|
||||
$project = $consoleDB->getDocument('projects', $project->getId());
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expired = DateTime::addSeconds(new \DateTime(), -1 * $duration);
|
||||
|
||||
|
|
@ -465,8 +518,8 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
protected function deleteRealtimeUsage(string $datetime): void
|
||||
{
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
// Delete Dead Realtime Logs
|
||||
$this->deleteByGroup('realtime', [
|
||||
Query::lessThan('timestamp', $datetime)
|
||||
|
|
@ -484,8 +537,9 @@ class DeletesV1 extends Worker
|
|||
throw new Exception('Failed to delete audit logs. No datetime provided');
|
||||
}
|
||||
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($datetime) {
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$timeLimit = new TimeLimit("", 0, 1, $dbForProject);
|
||||
$abuse = new Abuse($timeLimit);
|
||||
$status = $abuse->cleanup($datetime);
|
||||
|
|
@ -505,8 +559,9 @@ class DeletesV1 extends Worker
|
|||
throw new Exception('Failed to delete audit logs. No datetime provided');
|
||||
}
|
||||
|
||||
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$this->deleteForProjectIds(function (Document $project) use ($datetime) {
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$audit = new Audit($dbForProject);
|
||||
$status = $audit->cleanup($datetime);
|
||||
if (!$status) {
|
||||
|
|
@ -517,12 +572,11 @@ class DeletesV1 extends Worker
|
|||
|
||||
/**
|
||||
* @param string $resource
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteAuditLogsByResource(string $resource, string $projectId): void
|
||||
protected function deleteAuditLogsByResource(string $resource, Document $project): void
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$this->deleteByGroup(Audit::COLLECTION, [
|
||||
Query::equal('resource', [$resource])
|
||||
|
|
@ -531,20 +585,35 @@ class DeletesV1 extends Worker
|
|||
|
||||
/**
|
||||
* @param Document $document function document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteFunction(Document $document, string $projectId): void
|
||||
protected function deleteFunction(Document $document, Document $project): void
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
$functionId = $document->getId();
|
||||
$functionInternalId = $document->getInternalId();
|
||||
|
||||
/**
|
||||
* Delete rules
|
||||
*/
|
||||
Console::info("Deleting rules for function " . $functionId);
|
||||
$this->deleteByGroup('rules', [
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceInternalId', [$functionInternalId]),
|
||||
Query::equal('projectInternalId', [$project->getInternalId()])
|
||||
], $dbForConsole, function (Document $document) use ($project) {
|
||||
$this->deleteRule($document, $project);
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Variables
|
||||
*/
|
||||
Console::info("Deleting variables for function " . $functionId);
|
||||
$this->deleteByGroup('variables', [
|
||||
Query::equal('functionId', [$functionId])
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceInternalId', [$functionInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
/**
|
||||
|
|
@ -552,11 +621,11 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
Console::info("Deleting deployments for function " . $functionId);
|
||||
$storageFunctions = $this->getFunctionsDevice($projectId);
|
||||
$deploymentIds = [];
|
||||
$deploymentInternalIds = [];
|
||||
$this->deleteByGroup('deployments', [
|
||||
Query::equal('resourceId', [$functionId])
|
||||
], $dbForProject, function (Document $document) use ($storageFunctions, &$deploymentIds) {
|
||||
$deploymentIds[] = $document->getId();
|
||||
Query::equal('resourceInternalId', [$functionInternalId])
|
||||
], $dbForProject, function (Document $document) use ($storageFunctions, &$deploymentInternalIds) {
|
||||
$deploymentInternalIds[] = $document->getInternalId();
|
||||
if ($storageFunctions->delete($document->getAttribute('path', ''), true)) {
|
||||
Console::success('Deleted deployment files: ' . $document->getAttribute('path', ''));
|
||||
} else {
|
||||
|
|
@ -569,14 +638,14 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
Console::info("Deleting builds for function " . $functionId);
|
||||
$storageBuilds = $this->getBuildsDevice($projectId);
|
||||
foreach ($deploymentIds as $deploymentId) {
|
||||
foreach ($deploymentInternalIds as $deploymentInternalId) {
|
||||
$this->deleteByGroup('builds', [
|
||||
Query::equal('deploymentId', [$deploymentId])
|
||||
], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId) {
|
||||
if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) {
|
||||
Console::success('Deleted build files: ' . $document->getAttribute('outputPath', ''));
|
||||
Query::equal('deploymentInternalId', [$deploymentInternalId])
|
||||
], $dbForProject, function (Document $document) use ($storageBuilds) {
|
||||
if ($storageBuilds->delete($document->getAttribute('path', ''), true)) {
|
||||
Console::success('Deleted build files: ' . $document->getAttribute('path', ''));
|
||||
} else {
|
||||
Console::error('Failed to delete build files: ' . $document->getAttribute('outputPath', ''));
|
||||
Console::error('Failed to delete build files: ' . $document->getAttribute('path', ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -586,33 +655,26 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
Console::info("Deleting executions for function " . $functionId);
|
||||
$this->deleteByGroup('executions', [
|
||||
Query::equal('functionId', [$functionId])
|
||||
Query::equal('functionInternalId', [$functionInternalId])
|
||||
], $dbForProject);
|
||||
|
||||
/**
|
||||
* Request executor to delete all deployment containers
|
||||
*/
|
||||
Console::info("Requesting executor to delete all deployment containers for function " . $functionId);
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
foreach ($deploymentIds as $deploymentId) {
|
||||
try {
|
||||
$executor->deleteRuntime($projectId, $deploymentId);
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
}
|
||||
$this->deleteRuntimes($document, $project);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document $document deployment document
|
||||
* @param string $projectId
|
||||
* @throws Exception
|
||||
* @param Document $project
|
||||
*/
|
||||
protected function deleteDeployment(Document $document, string $projectId): void
|
||||
protected function deleteDeployment(Document $document, Document $project): void
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$deploymentId = $document->getId();
|
||||
$functionId = $document->getAttribute('resourceId');
|
||||
$deploymentInternalId = $document->getInternalId();
|
||||
|
||||
/**
|
||||
* Delete deployment files
|
||||
|
|
@ -631,25 +693,21 @@ class DeletesV1 extends Worker
|
|||
Console::info("Deleting builds for deployment " . $deploymentId);
|
||||
$storageBuilds = $this->getBuildsDevice($projectId);
|
||||
$this->deleteByGroup('builds', [
|
||||
Query::equal('deploymentId', [$deploymentId])
|
||||
Query::equal('deploymentInternalId', [$deploymentInternalId])
|
||||
], $dbForProject, function (Document $document) use ($storageBuilds) {
|
||||
if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) {
|
||||
Console::success('Deleted build files: ' . $document->getAttribute('outputPath', ''));
|
||||
if ($storageBuilds->delete($document->getAttribute('path', ''), true)) {
|
||||
Console::success('Deleted build files: ' . $document->getAttribute('path', ''));
|
||||
} else {
|
||||
Console::error('Failed to delete build files: ' . $document->getAttribute('outputPath', ''));
|
||||
Console::error('Failed to delete build files: ' . $document->getAttribute('path', ''));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Request executor to delete the deployment container
|
||||
* Request executor to delete all deployment containers
|
||||
*/
|
||||
Console::info("Requesting executor to delete deployment container for deployment " . $deploymentId);
|
||||
try {
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
$executor->deleteRuntime($projectId, $deploymentId);
|
||||
} catch (Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
$this->deleteRuntimes($document, $project);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -683,6 +741,7 @@ class DeletesV1 extends Worker
|
|||
*/
|
||||
protected function deleteForProjectIds(callable $callback): void
|
||||
{
|
||||
// TODO: @Meldiron name of this method no longer matches. It does not delete, and it gives whole document
|
||||
$count = 0;
|
||||
$chunk = 0;
|
||||
$limit = 50;
|
||||
|
|
@ -697,13 +756,11 @@ class DeletesV1 extends Worker
|
|||
$chunk++;
|
||||
|
||||
/** @var string[] $projectIds */
|
||||
$projectIds = array_map(fn (Document $project) => $project->getId(), $projects);
|
||||
|
||||
$sum = count($projects);
|
||||
|
||||
Console::info('Executing delete function for chunk #' . $chunk . '. Found ' . $sum . ' projects');
|
||||
foreach ($projectIds as $projectId) {
|
||||
$callback($projectId);
|
||||
foreach ($projects as $project) {
|
||||
$callback($project);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -729,19 +786,23 @@ class DeletesV1 extends Worker
|
|||
|
||||
$executionStart = \microtime(true);
|
||||
|
||||
while ($sum === $limit) {
|
||||
$chunk++;
|
||||
try {
|
||||
while ($sum === $limit) {
|
||||
$chunk++;
|
||||
|
||||
$results = $database->find($collection, \array_merge([Query::limit($limit)], $queries));
|
||||
$results = $database->find($collection, \array_merge([Query::limit($limit)], $queries));
|
||||
|
||||
$sum = count($results);
|
||||
$sum = count($results);
|
||||
|
||||
Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents');
|
||||
Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents in collection ' . $database->getNamespace() . '_' . $collection);
|
||||
|
||||
foreach ($results as $document) {
|
||||
$this->deleteById($document, $database, $callback);
|
||||
$count++;
|
||||
foreach ($results as $document) {
|
||||
$this->deleteById($document, $database, $callback);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
|
||||
$executionEnd = \microtime(true);
|
||||
|
|
@ -750,44 +811,65 @@ class DeletesV1 extends Worker
|
|||
}
|
||||
|
||||
/**
|
||||
* @param Document $document certificates document
|
||||
* @throws \Utopia\Database\Exception\Authorization
|
||||
* @param string $collection collectionID
|
||||
* @param Query[] $queries
|
||||
* @param Database $database
|
||||
* @param callable $callback
|
||||
*/
|
||||
protected function deleteCertificates(Document $document): void
|
||||
protected function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
$count = 0;
|
||||
$chunk = 0;
|
||||
$limit = 50;
|
||||
$results = [];
|
||||
$sum = $limit;
|
||||
$cursor = null;
|
||||
|
||||
// If domain has certificate generated
|
||||
if (isset($document['certificateId'])) {
|
||||
$domainUsingCertificate = $consoleDB->findOne('domains', [
|
||||
Query::equal('certificateId', [$document['certificateId']])
|
||||
]);
|
||||
$executionStart = \microtime(true);
|
||||
|
||||
if (!$domainUsingCertificate) {
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN_TARGET', '');
|
||||
if ($mainDomain === $document->getAttribute('domain')) {
|
||||
$domainUsingCertificate = $mainDomain;
|
||||
}
|
||||
while ($sum === $limit) {
|
||||
$chunk++;
|
||||
|
||||
$mergedQueries = \array_merge([Query::limit($limit)], $queries);
|
||||
if ($cursor instanceof Document) {
|
||||
$mergedQueries[] = Query::cursorAfter($cursor);
|
||||
}
|
||||
|
||||
// If certificate is still used by some domain, mark we can't delete.
|
||||
// Current domain should not be found, because we only have copy. Original domain is already deleted from database.
|
||||
if ($domainUsingCertificate) {
|
||||
Console::warning("Skipping certificate deletion, because a domain is still using it.");
|
||||
return;
|
||||
$results = $database->find($collection, $mergedQueries);
|
||||
|
||||
$sum = count($results);
|
||||
|
||||
if ($sum > 0) {
|
||||
$cursor = $results[$sum - 1];
|
||||
}
|
||||
|
||||
foreach ($results as $document) {
|
||||
if (is_callable($callback)) {
|
||||
$callback($document);
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$executionEnd = \microtime(true);
|
||||
|
||||
Console::info("Listed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Document $document rule document
|
||||
* @param Document $project project document
|
||||
*/
|
||||
protected function deleteRule(Document $document, Document $project): void
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
|
||||
$domain = $document->getAttribute('domain');
|
||||
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
|
||||
$checkTraversal = realpath($directory) === $directory;
|
||||
|
||||
if ($domain && $checkTraversal && is_dir($directory)) {
|
||||
// Delete certificate document, so Appwrite is aware of change
|
||||
if (isset($document['certificateId'])) {
|
||||
$consoleDB->deleteDocument('certificates', $document['certificateId']);
|
||||
}
|
||||
|
||||
if ($checkTraversal && is_dir($directory)) {
|
||||
// Delete files, so Traefik is aware of change
|
||||
array_map('unlink', glob($directory . '/*.*'));
|
||||
rmdir($directory);
|
||||
|
|
@ -795,15 +877,89 @@ class DeletesV1 extends Worker
|
|||
} else {
|
||||
Console::info("No certificate files found for {$domain}");
|
||||
}
|
||||
|
||||
// Delete certificate document, so Appwrite is aware of change
|
||||
if (isset($document['certificateId'])) {
|
||||
$consoleDB->deleteDocument('certificates', $document['certificateId']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function deleteBucket(Document $document, string $projectId)
|
||||
protected function deleteBucket(Document $document, Document $project)
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($projectId);
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$dbForProject->deleteCollection('bucket_' . $document->getInternalId());
|
||||
|
||||
$device = $this->getFilesDevice($projectId);
|
||||
|
||||
$device->deletePath($document->getId());
|
||||
}
|
||||
|
||||
protected function deleteInstallation(Document $document, Document $project)
|
||||
{
|
||||
$dbForProject = $this->getProjectDB($project);
|
||||
$dbForConsole = $this->getConsoleDB();
|
||||
|
||||
$this->listByGroup('functions', [
|
||||
Query::equal('installationInternalId', [$document->getInternalId()])
|
||||
], $dbForProject, function ($function) use ($dbForProject, $dbForConsole) {
|
||||
$dbForConsole->deleteDocument('repositories', $function->getAttribute('repositoryId'));
|
||||
|
||||
$function = $function
|
||||
->setAttribute('installationId', '')
|
||||
->setAttribute('installationInternalId', '')
|
||||
->setAttribute('providerRepositoryId', '')
|
||||
->setAttribute('providerBranch', '')
|
||||
->setAttribute('providerSilentMode', false)
|
||||
->setAttribute('providerRootDirectory', '')
|
||||
->setAttribute('repositoryId', '')
|
||||
->setAttribute('repositoryInternalId', '');
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
});
|
||||
}
|
||||
|
||||
protected function deleteRuntimes(?Document $function, Document $project)
|
||||
{
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
|
||||
$deleteByFunction = function (Document $function) use ($project, $executor) {
|
||||
$this->listByGroup(
|
||||
'deployments',
|
||||
[
|
||||
Query::equal('resourceInternalId', [$function->getInternalId()]),
|
||||
Query::equal('resourceType', ['functions']),
|
||||
],
|
||||
$this->getProjectDB($project),
|
||||
function (Document $deployment) use ($project, $executor) {
|
||||
$deploymentId = $deployment->getId();
|
||||
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $deploymentId);
|
||||
Console::info("Runtime for deployment {$deploymentId} deleted.");
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Runtime for deployment {$deploymentId} skipped:");
|
||||
Console::error('[Error] Type: ' . get_class($th));
|
||||
Console::error('[Error] Message: ' . $th->getMessage());
|
||||
Console::error('[Error] File: ' . $th->getFile());
|
||||
Console::error('[Error] Line: ' . $th->getLine());
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if ($function !== null) {
|
||||
// Delete function runtimes
|
||||
$deleteByFunction($function);
|
||||
} else {
|
||||
// Delete all project runtimes
|
||||
$this->listByGroup(
|
||||
'functions',
|
||||
[],
|
||||
$this->getProjectDB($project),
|
||||
function (Document $function) use ($deleteByFunction) {
|
||||
$deleteByFunction($function);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,326 +1,227 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../worker.php';
|
||||
|
||||
use Domnikl\Statsd\Client;
|
||||
use Utopia\Queue\Message;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Cron\CronExpression;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
Authorization::disable();
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
Console::title('Functions V1 Worker');
|
||||
Console::success(APP_NAME . ' functions worker v1 has started');
|
||||
|
||||
class FunctionsV1 extends Worker
|
||||
{
|
||||
private ?Executor $executor = null;
|
||||
public array $args = [];
|
||||
public array $allowed = [];
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "functions";
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$this->executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$type = $this->args['type'] ?? '';
|
||||
$events = $this->args['events'] ?? [];
|
||||
$project = new Document($this->args['project'] ?? []);
|
||||
$user = new Document($this->args['user'] ?? []);
|
||||
$payload = json_encode($this->args['payload'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
$database = $this->getProjectDB($project->getId());
|
||||
|
||||
/**
|
||||
* Handle Event execution.
|
||||
*/
|
||||
if (!empty($events)) {
|
||||
$limit = 30;
|
||||
$sum = 30;
|
||||
$offset = 0;
|
||||
$functions = [];
|
||||
/** @var Document[] $functions */
|
||||
|
||||
while ($sum >= $limit) {
|
||||
$functions = $database->find('functions', [
|
||||
Query::limit($limit),
|
||||
Query::offset($offset),
|
||||
Query::orderAsc('name'),
|
||||
]);
|
||||
$sum = \count($functions);
|
||||
$offset = $offset + $limit;
|
||||
|
||||
Console::log('Fetched ' . $sum . ' functions...');
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!array_intersect($events, $function->getAttribute('events', []))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::success('Iterating function: ' . $function->getAttribute('name'));
|
||||
|
||||
try {
|
||||
$this->execute(
|
||||
project: $project,
|
||||
function: $function,
|
||||
dbForProject: $database,
|
||||
trigger: 'event',
|
||||
// Pass first, most verbose event pattern
|
||||
event: $events[0],
|
||||
eventData: $payload,
|
||||
user: $user
|
||||
);
|
||||
|
||||
Console::success('Triggered function: ' . $events[0]);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error("Failed to execute " . $function->getId() . " with error: " . $th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Schedule and HTTP execution.
|
||||
*/
|
||||
$user = new Document($this->args['user'] ?? []);
|
||||
$project = new Document($this->args['project'] ?? []);
|
||||
$execution = new Document($this->args['execution'] ?? []);
|
||||
$function = new Document($this->args['function'] ?? []);
|
||||
|
||||
switch ($type) {
|
||||
case 'http':
|
||||
$jwt = $this->args['jwt'] ?? '';
|
||||
$data = $this->args['data'] ?? '';
|
||||
|
||||
$function = $database->getDocument('functions', $execution->getAttribute('functionId'));
|
||||
|
||||
$this->execute(
|
||||
project: $project,
|
||||
function: $function,
|
||||
dbForProject: $database,
|
||||
executionId: $execution->getId(),
|
||||
trigger: 'http',
|
||||
data: $data,
|
||||
user: $user,
|
||||
jwt: $jwt
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'schedule':
|
||||
$functionOriginal = $function;
|
||||
/*
|
||||
* 1. Get Original Task
|
||||
* 2. Check for updates
|
||||
* If has updates skip task and don't reschedule
|
||||
* If status not equal to play skip task
|
||||
* 3. Check next run date, update task and add new job at the given date
|
||||
* 4. Execute task (set optional timeout)
|
||||
* 5. Update task response to log
|
||||
* On success reset error count
|
||||
* On failure add error count
|
||||
* If error count bigger than allowed change status to pause
|
||||
*/
|
||||
|
||||
// Reschedule
|
||||
$function = $database->getDocument('functions', $function->getId());
|
||||
|
||||
if (empty($function->getId())) {
|
||||
throw new Exception('Function not found (' . $function->getId() . ')');
|
||||
}
|
||||
|
||||
if ($functionOriginal->getAttribute('schedule') !== $function->getAttribute('schedule')) { // Schedule has changed from previous run, ignore this run.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($functionOriginal->getAttribute('scheduleUpdatedAt') !== $function->getAttribute('scheduleUpdatedAt')) { // Double execution due to rapid cron changes, ignore this run.
|
||||
return;
|
||||
}
|
||||
|
||||
$cron = new CronExpression($function->getAttribute('schedule'));
|
||||
$next = DateTime::format($cron->getNextRunDate());
|
||||
|
||||
$function = $function
|
||||
->setAttribute('scheduleNext', $next)
|
||||
->setAttribute('schedulePrevious', DateTime::now());
|
||||
|
||||
$function = $database->updateDocument(
|
||||
'functions',
|
||||
$function->getId(),
|
||||
$function
|
||||
);
|
||||
|
||||
$reschedule = new Func();
|
||||
$reschedule
|
||||
->setFunction($function)
|
||||
->setType('schedule')
|
||||
->setUser($user)
|
||||
->setProject($project)
|
||||
->schedule(new \DateTime($next));
|
||||
;
|
||||
|
||||
$this->execute(
|
||||
project: $project,
|
||||
function: $function,
|
||||
dbForProject: $database,
|
||||
trigger: 'schedule'
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function execute(
|
||||
Server::setResource('execute', function () {
|
||||
return function (
|
||||
Log $log,
|
||||
Func $queueForFunctions,
|
||||
Database $dbForProject,
|
||||
Client $statsd,
|
||||
Document $project,
|
||||
Document $function,
|
||||
Database $dbForProject,
|
||||
string $trigger,
|
||||
string $executionId = null,
|
||||
string $data = null,
|
||||
string $path,
|
||||
string $method,
|
||||
array $headers,
|
||||
?Document $user = null,
|
||||
string $jwt = null,
|
||||
string $event = null,
|
||||
string $eventData = null,
|
||||
string $data = null,
|
||||
?Document $user = null,
|
||||
string $jwt = null
|
||||
string $executionId = null,
|
||||
) {
|
||||
|
||||
$user ??= new Document();
|
||||
$functionId = $function->getId();
|
||||
$deploymentId = $function->getAttribute('deployment', '');
|
||||
|
||||
$log->addTag('functionId', $functionId);
|
||||
$log->addTag('projectId', $project->getId());
|
||||
|
||||
/** Check if deployment exists */
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $functionId) {
|
||||
throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404);
|
||||
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404);
|
||||
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
/** Check if build has exists */
|
||||
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
|
||||
if ($build->isEmpty()) {
|
||||
throw new Exception('Build not found', 404);
|
||||
throw new Exception('Build not found');
|
||||
}
|
||||
|
||||
if ($build->getAttribute('status') !== 'ready') {
|
||||
throw new Exception('Build not ready', 400);
|
||||
throw new Exception('Build not ready');
|
||||
}
|
||||
|
||||
/** Check if runtime is supported */
|
||||
$runtimes = Config::getParam('runtimes', []);
|
||||
|
||||
if (!\array_key_exists($function->getAttribute('runtime'), $runtimes)) {
|
||||
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400);
|
||||
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
$runtime = $runtimes[$function->getAttribute('runtime')];
|
||||
|
||||
$headers['x-appwrite-trigger'] = $trigger;
|
||||
$headers['x-appwrite-event'] = $event ?? '';
|
||||
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
|
||||
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
|
||||
|
||||
/** Create execution or update execution status */
|
||||
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
|
||||
if ($execution->isEmpty()) {
|
||||
$headersFiltered = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
|
||||
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
|
||||
}
|
||||
}
|
||||
|
||||
$executionId = ID::unique();
|
||||
$execution = $dbForProject->createDocument('executions', new Document([
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
|
||||
'functionId' => $functionId,
|
||||
'deploymentId' => $deploymentId,
|
||||
'trigger' => $trigger,
|
||||
'status' => 'waiting',
|
||||
'statusCode' => 0,
|
||||
'response' => '',
|
||||
'stderr' => '',
|
||||
'functionInternalId' => $function->getInternalId(),
|
||||
'functionId' => $function->getId(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => 'http',
|
||||
'status' => 'processing',
|
||||
'responseStatusCode' => 0,
|
||||
'responseHeaders' => [],
|
||||
'requestPath' => $path,
|
||||
'requestMethod' => $method,
|
||||
'requestHeaders' => $headersFiltered,
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
'search' => implode(' ', [$functionId, $executionId]),
|
||||
]));
|
||||
]);
|
||||
|
||||
if ($function->getAttribute('logging')) {
|
||||
$execution = $dbForProject->createDocument('executions', $execution);
|
||||
}
|
||||
|
||||
// TODO: @Meldiron Trigger executions.create event here
|
||||
|
||||
if ($execution->isEmpty()) {
|
||||
throw new Exception('Failed to create or read execution');
|
||||
}
|
||||
}
|
||||
$execution->setAttribute('status', 'processing');
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
|
||||
$vars = array_reduce($function['vars'] ?? [], function (array $carry, Document $var) {
|
||||
if ($execution->getAttribute('status') !== 'processing') {
|
||||
$execution->setAttribute('status', 'processing');
|
||||
|
||||
if ($function->getAttribute('logging')) {
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
}
|
||||
}
|
||||
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
$vars = [];
|
||||
|
||||
// Shared vars
|
||||
$varsShared = $project->getAttribute('variables', []);
|
||||
$vars = \array_merge($vars, \array_reduce($varsShared, function (array $carry, Document $var) {
|
||||
$carry[$var->getAttribute('key')] = $var->getAttribute('value') ?? '';
|
||||
return $carry;
|
||||
}, []));
|
||||
|
||||
// Function vars
|
||||
$vars = \array_merge($vars, array_reduce($function->getAttribute('vars', []), function (array $carry, Document $var) {
|
||||
$carry[$var->getAttribute('key')] = $var->getAttribute('value');
|
||||
return $carry;
|
||||
}, []);
|
||||
}, []));
|
||||
|
||||
/** Collect environment variables */
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_ID' => $functionId,
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deploymentId,
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $trigger,
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_FUNCTION_EVENT' => $event ?? '',
|
||||
'APPWRITE_FUNCTION_EVENT_DATA' => $eventData ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $data ?? '',
|
||||
'APPWRITE_FUNCTION_USER_ID' => $user->getId() ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $jwt ?? '',
|
||||
]);
|
||||
|
||||
$body = $eventData ?? '';
|
||||
if (empty($body)) {
|
||||
$body = $data ?? '';
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
try {
|
||||
$executionResponse = $this->executor->createExecution(
|
||||
$command = $runtime['startCommand'];
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deploymentId,
|
||||
path: $build->getAttribute('outputPath', ''),
|
||||
vars: $vars,
|
||||
entrypoint: $deployment->getAttribute('entrypoint', ''),
|
||||
data: $vars['APPWRITE_FUNCTION_DATA'] ?? '',
|
||||
runtime: $function->getAttribute('runtime', ''),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $function->getAttribute('timeout', 0),
|
||||
baseImage: $runtime['image']
|
||||
image: $runtime['image'],
|
||||
source: $build->getAttribute('path', ''),
|
||||
entrypoint: $deployment->getAttribute('entrypoint', ''),
|
||||
version: $function->getAttribute('version'),
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'
|
||||
);
|
||||
|
||||
$status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed';
|
||||
|
||||
$headersFiltered = [];
|
||||
foreach ($executionResponse['headers'] as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
|
||||
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
|
||||
}
|
||||
}
|
||||
|
||||
/** Update execution status */
|
||||
$execution
|
||||
->setAttribute('status', $executionResponse['status'])
|
||||
->setAttribute('statusCode', $executionResponse['statusCode'])
|
||||
->setAttribute('response', $executionResponse['response'])
|
||||
->setAttribute('stdout', $executionResponse['stdout'])
|
||||
->setAttribute('stderr', $executionResponse['stderr'])
|
||||
->setAttribute('status', $status)
|
||||
->setAttribute('responseStatusCode', $executionResponse['statusCode'])
|
||||
->setAttribute('responseHeaders', $headersFiltered)
|
||||
->setAttribute('logs', $executionResponse['logs'])
|
||||
->setAttribute('errors', $executionResponse['errors'])
|
||||
->setAttribute('duration', $executionResponse['duration']);
|
||||
} catch (\Throwable $th) {
|
||||
$interval = (new \DateTime())->diff(new \DateTime($execution->getCreatedAt()));
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
$execution
|
||||
->setAttribute('duration', (float)$interval->format('%s.%f'))
|
||||
->setAttribute('duration', $durationEnd - $durationStart)
|
||||
->setAttribute('status', 'failed')
|
||||
->setAttribute('statusCode', $th->getCode())
|
||||
->setAttribute('stderr', $th->getMessage());
|
||||
Console::error($th->getMessage());
|
||||
->setAttribute('responseStatusCode', 500)
|
||||
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
|
||||
|
||||
$error = $th->getMessage();
|
||||
$errorCode = $th->getCode();
|
||||
}
|
||||
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
if ($function->getAttribute('logging')) {
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
}
|
||||
|
||||
/** Trigger Webhook */
|
||||
$executionModel = new Execution();
|
||||
|
|
@ -335,9 +236,8 @@ class FunctionsV1 extends Worker
|
|||
->trigger();
|
||||
|
||||
/** Trigger Functions */
|
||||
$executionUpdate
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
$queueForFunctions
|
||||
->from($executionUpdate)
|
||||
->trigger();
|
||||
|
||||
/** Trigger realtime event */
|
||||
|
|
@ -366,14 +266,12 @@ class FunctionsV1 extends Worker
|
|||
);
|
||||
|
||||
/** Update usage stats */
|
||||
global $register;
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
|
||||
$statsd = $register->get('statsd');
|
||||
$usage = new Stats($statsd);
|
||||
$usage
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('projectInternalId', $project->getInternalId())
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('functionId', $function->getId()) // TODO: We should use functionInternalId in usage stats
|
||||
->setParam('executions.{scope}.compute', 1)
|
||||
->setParam('executionStatus', $execution->getAttribute('status', ''))
|
||||
->setParam('executionTime', $execution->getAttribute('duration'))
|
||||
|
|
@ -381,9 +279,135 @@ class FunctionsV1 extends Worker
|
|||
->setParam('networkResponseSize', 0)
|
||||
->submit();
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
if (!empty($error)) {
|
||||
throw new Exception($error, $errorCode);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$server->job()
|
||||
->inject('message')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForFunctions')
|
||||
->inject('statsd')
|
||||
->inject('execute')
|
||||
->inject('log')
|
||||
->action(function (Message $message, Database $dbForProject, Func $queueForFunctions, Client $statsd, callable $execute, Log $log) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$type = $payload['type'] ?? '';
|
||||
$events = $payload['events'] ?? [];
|
||||
$data = $payload['body'] ?? '';
|
||||
$eventData = $payload['payload'] ?? '';
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
$function = new Document($payload['function'] ?? []);
|
||||
$user = new Document($payload['user'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($events)) {
|
||||
$limit = 30;
|
||||
$sum = 30;
|
||||
$offset = 0;
|
||||
$functions = [];
|
||||
/** @var Document[] $functions */
|
||||
while ($sum >= $limit) {
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit($limit),
|
||||
Query::offset($offset)
|
||||
]);
|
||||
|
||||
$sum = \count($functions);
|
||||
$offset = $offset + $limit;
|
||||
|
||||
Console::log('Fetched ' . $sum . ' functions...');
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!array_intersect($events, $function->getAttribute('events', []))) {
|
||||
continue;
|
||||
}
|
||||
Console::success('Iterating function: ' . $function->getAttribute('name'));
|
||||
$execute(
|
||||
statsd: $statsd,
|
||||
dbForProject: $dbForProject,
|
||||
project: $project,
|
||||
function: $function,
|
||||
queueForFunctions: $queueForFunctions,
|
||||
trigger: 'event',
|
||||
event: $events[0],
|
||||
eventData: \is_string($eventData) ? $eventData : \json_encode($eventData),
|
||||
user: $user,
|
||||
data: null,
|
||||
executionId: null,
|
||||
jwt: null,
|
||||
path: '/',
|
||||
method: 'POST',
|
||||
headers: [
|
||||
'user-agent' => 'Appwrite/' . APP_VERSION_STABLE
|
||||
],
|
||||
);
|
||||
Console::success('Triggered function: ' . $events[0]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Schedule and HTTP execution.
|
||||
*/
|
||||
switch ($type) {
|
||||
case 'http':
|
||||
$jwt = $payload['jwt'] ?? '';
|
||||
$execution = new Document($payload['execution'] ?? []);
|
||||
$user = new Document($payload['user'] ?? []);
|
||||
$execute(
|
||||
log: $log,
|
||||
project: $project,
|
||||
function: $function,
|
||||
dbForProject: $dbForProject,
|
||||
queueForFunctions: $queueForFunctions,
|
||||
trigger: 'http',
|
||||
executionId: $execution->getId(),
|
||||
event: null,
|
||||
eventData: null,
|
||||
data: $data,
|
||||
user: $user,
|
||||
jwt: $jwt,
|
||||
path: $payload['path'],
|
||||
method: $payload['method'],
|
||||
headers: $payload['headers'],
|
||||
statsd: $statsd,
|
||||
);
|
||||
break;
|
||||
case 'schedule':
|
||||
$execute(
|
||||
log: $log,
|
||||
project: $project,
|
||||
function: $function,
|
||||
dbForProject: $dbForProject,
|
||||
queueForFunctions: $queueForFunctions,
|
||||
trigger: 'schedule',
|
||||
executionId: null,
|
||||
event: null,
|
||||
eventData: null,
|
||||
data: null,
|
||||
user: null,
|
||||
jwt: null,
|
||||
path: $payload['path'],
|
||||
method: $payload['method'],
|
||||
headers: $payload['headers'],
|
||||
statsd: $statsd,
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$server->workerStart();
|
||||
$server->start();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Template\Template;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
|
|
@ -24,30 +26,31 @@ class MailsV1 extends Worker
|
|||
{
|
||||
global $register;
|
||||
|
||||
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
Console::info('Skipped mail processing. No SMTP server hostname has been set.');
|
||||
$smtp = $this->args['smtp'];
|
||||
|
||||
if (empty($smtp) && empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
Console::info('Skipped mail processing. No SMTP configuration has been set.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$recipient = $this->args['recipient'];
|
||||
$subject = $this->args['subject'];
|
||||
$name = $this->args['name'];
|
||||
$body = $this->args['body'];
|
||||
$from = $this->args['from'];
|
||||
$variables = $this->args['variables'];
|
||||
|
||||
/** @var \PHPMailer\PHPMailer\PHPMailer $mail */
|
||||
$mail = $register->get('smtp');
|
||||
$body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl');
|
||||
|
||||
// Set project mail
|
||||
/*$register->get('smtp')
|
||||
->setFrom(
|
||||
App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
|
||||
($project->getId() === 'console')
|
||||
? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server'))
|
||||
: \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name')
|
||||
)
|
||||
);*/
|
||||
foreach ($variables as $key => $value) {
|
||||
$body->setParam('{{' . $key . '}}', $value);
|
||||
}
|
||||
|
||||
$body = $body->render();
|
||||
|
||||
/** @var PHPMailer $mail */
|
||||
$mail = empty($smtp)
|
||||
? $register->get('smtp')
|
||||
: $this->getMailer($smtp);
|
||||
|
||||
$mail->clearAddresses();
|
||||
$mail->clearAllRecipients();
|
||||
|
|
@ -55,8 +58,6 @@ class MailsV1 extends Worker
|
|||
$mail->clearAttachments();
|
||||
$mail->clearBCCs();
|
||||
$mail->clearCCs();
|
||||
|
||||
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), (empty($from) ? \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')) : $from));
|
||||
$mail->addAddress($recipient, $name);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $body;
|
||||
|
|
@ -69,6 +70,36 @@ class MailsV1 extends Worker
|
|||
}
|
||||
}
|
||||
|
||||
protected function getMailer(array $smtp): PHPMailer
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = $smtp['username'];
|
||||
$password = $smtp['password'];
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = $smtp['host'];
|
||||
$mail->Port = $smtp['port'];
|
||||
$mail->SMTPAuth = (!empty($username) && !empty($password));
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = $smtp['secure'];
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
|
||||
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$mail->addReplyTo($smtp['replyTo'], $smtp['senderName']);
|
||||
}
|
||||
|
||||
$mail->isHTML();
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\DSN\DSN;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Messaging\Adapter;
|
||||
use Utopia\Messaging\Adapters\SMS\Mock;
|
||||
use Utopia\Messaging\Adapters\SMS\Msg91;
|
||||
|
|
|
|||
308
app/workers/migrations.php
Normal file
308
app/workers/migrations.php
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Role;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Migration\Destinations\Appwrite as DestinationsAppwrite;
|
||||
use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Source;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
Console::title('Migrations V1 Worker');
|
||||
Console::success(APP_NAME . ' Migrations worker v1 has started');
|
||||
|
||||
class MigrationsV1 extends Worker
|
||||
{
|
||||
/**
|
||||
* Database connection shared across all methods of this file
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
private Database $dbForProject;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'migrations';
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$type = $this->args['type'] ?? '';
|
||||
$events = $this->args['events'] ?? [];
|
||||
$project = new Document($this->args['project'] ?? []);
|
||||
$user = new Document($this->args['user'] ?? []);
|
||||
$payload = json_encode($this->args['payload'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Event execution.
|
||||
*/
|
||||
if (! empty($events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dbForProject = $this->getProjectDB($project);
|
||||
|
||||
$this->processMigration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Source
|
||||
*
|
||||
* @return Source
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function processSource(string $source, array $credentials): Source
|
||||
{
|
||||
switch ($source) {
|
||||
case Firebase::getName():
|
||||
return new Firebase(
|
||||
json_decode($credentials['serviceAccount'], true),
|
||||
);
|
||||
break;
|
||||
case Supabase::getName():
|
||||
return new Supabase(
|
||||
$credentials['endpoint'],
|
||||
$credentials['apiKey'],
|
||||
$credentials['databaseHost'],
|
||||
'postgres',
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$credentials['port'],
|
||||
);
|
||||
break;
|
||||
case NHost::getName():
|
||||
return new NHost(
|
||||
$credentials['subdomain'],
|
||||
$credentials['region'],
|
||||
$credentials['adminSecret'],
|
||||
$credentials['database'],
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$credentials['port'],
|
||||
);
|
||||
break;
|
||||
case Appwrite::getName():
|
||||
return new Appwrite($credentials['projectId'], str_starts_with($credentials['endpoint'], 'http://localhost/v1') ? 'http://appwrite/v1' : $credentials['endpoint'], $credentials['apiKey']);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Invalid source type');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateMigrationDocument(Document $migration, Document $project): Document
|
||||
{
|
||||
/** Trigger Realtime */
|
||||
$allEvents = Event::generateEvents('migrations.[migrationId].update', [
|
||||
'migrationId' => $migration->getId(),
|
||||
]);
|
||||
|
||||
$target = Realtime::fromPayload(
|
||||
event: $allEvents[0],
|
||||
payload: $migration,
|
||||
project: $project
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $migration->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
);
|
||||
|
||||
Realtime::send(
|
||||
projectId: $project->getId(),
|
||||
payload: $migration->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
);
|
||||
|
||||
return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration);
|
||||
}
|
||||
|
||||
protected function removeAPIKey(Document $apiKey)
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
|
||||
$consoleDB->deleteDocument('keys', $apiKey->getId());
|
||||
}
|
||||
|
||||
protected function generateAPIKey(Document $project): Document
|
||||
{
|
||||
$consoleDB = $this->getConsoleDB();
|
||||
$generatedSecret = bin2hex(\random_bytes(128));
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'name' => 'Transfer API Key',
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
'users.write',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
'databases.read',
|
||||
'databases.write',
|
||||
'collections.read',
|
||||
'collections.write',
|
||||
'documents.read',
|
||||
'documents.write',
|
||||
'buckets.read',
|
||||
'buckets.write',
|
||||
'files.read',
|
||||
'files.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
],
|
||||
'expire' => null,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => $generatedSecret,
|
||||
]);
|
||||
|
||||
$consoleDB->createDocument('keys', $key);
|
||||
$consoleDB->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Migration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function processMigration(): void
|
||||
{
|
||||
/**
|
||||
* @var Document $migrationDocument
|
||||
* @var Transfer $transfer
|
||||
*/
|
||||
$migrationDocument = null;
|
||||
$transfer = null;
|
||||
$projectDocument = $this->getConsoleDB()->getDocument('projects', $this->args['project']['$id']);
|
||||
$tempAPIKey = $this->generateAPIKey($projectDocument);
|
||||
|
||||
try {
|
||||
$migrationDocument = $this->dbForProject->getDocument('migrations', $this->args['migration']['$id']);
|
||||
$migrationDocument->setAttribute('stage', 'processing');
|
||||
$migrationDocument->setAttribute('status', 'processing');
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
|
||||
$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials'));
|
||||
|
||||
$source->report();
|
||||
|
||||
$destination = new DestinationsAppwrite(
|
||||
$projectDocument->getId(),
|
||||
'http://appwrite/v1',
|
||||
$tempAPIKey['secret'],
|
||||
);
|
||||
|
||||
$transfer = new Transfer(
|
||||
$source,
|
||||
$destination
|
||||
);
|
||||
|
||||
/** Start Transfer */
|
||||
$migrationDocument->setAttribute('stage', 'migrating');
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
$transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) {
|
||||
$migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache()));
|
||||
$migrationDocument->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
|
||||
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
});
|
||||
|
||||
$errors = $transfer->getReport(Resource::STATUS_ERROR);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
|
||||
$errorMessages = [];
|
||||
foreach ($errors as $error) {
|
||||
$errorMessages[] = "Failed to transfer resource '{$error['id']}:{$error['resource']}' with message '{$error['message']}'";
|
||||
}
|
||||
|
||||
$migrationDocument->setAttribute('errors', $errorMessages);
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationDocument->setAttribute('status', 'completed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
|
||||
if ($migrationDocument) {
|
||||
Console::error($th->getMessage());
|
||||
Console::error($th->getTraceAsString());
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
$migrationDocument->setAttribute('errors', [$th->getMessage()]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($transfer) {
|
||||
$errors = $transfer->getReport(Resource::STATUS_ERROR);
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$migrationDocument->setAttribute('status', 'failed');
|
||||
$migrationDocument->setAttribute('stage', 'finished');
|
||||
$migrationDocument->setAttribute('errors', $errors);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($migrationDocument) {
|
||||
$this->updateMigrationDocument($migrationDocument, $projectDocument);
|
||||
}
|
||||
if ($tempAPIKey) {
|
||||
$this->removeAPIKey($tempAPIKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Verification
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function processVerification(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
3
bin/calc-tier-stats
Normal file
3
bin/calc-tier-stats
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php calc-tier-stats $@
|
||||
3
bin/calc-users-stats
Normal file
3
bin/calc-users-stats
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php calc-users-stats $@
|
||||
3
bin/clear-card-cache
Normal file
3
bin/clear-card-cache
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php clear-card-cache $@
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
php -e /usr/src/code/app/executor.php -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
||||
3
bin/hamster
Normal file
3
bin/hamster
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php hamster $@
|
||||
3
bin/patch-delete-project-collections
Normal file
3
bin/patch-delete-project-collections
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php patch-delete-project-collections $@
|
||||
3
bin/patch-delete-schedule-updated-at-attribute
Normal file
3
bin/patch-delete-schedule-updated-at-attribute
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php patch-delete-schedule-updated-at-attribute $@
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=1 REDIS_BACKEND=$REDIS_BACKEND RESQUE_PHP='/usr/src/code/vendor/autoload.php' php /usr/src/code/vendor/bin/resque-scheduler
|
||||
php /usr/src/code/app/cli.php schedule $@
|
||||
0
bin/usage
Executable file → Normal file
0
bin/usage
Executable file → Normal file
3
bin/volume-sync
Normal file
3
bin/volume-sync
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php volume-sync $@
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=0.1 QUEUE='v1-functions' APP_INCLUDE='/usr/src/code/app/workers/functions.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
||||
QUEUE=v1-functions php /usr/src/code/app/workers/functions.php $@
|
||||
10
bin/worker-migrations
Normal file
10
bin/worker-migrations
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=0.1 QUEUE='v1-migrations' APP_INCLUDE='/usr/src/code/app/workers/migrations.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
||||
|
|
@ -41,36 +41,43 @@
|
|||
"ext-openssl": "*",
|
||||
"ext-zlib": "*",
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-clamav": "1.1.*",
|
||||
"appwrite/php-runtimes": "0.11.*",
|
||||
"utopia-php/abuse": "0.25.*",
|
||||
"utopia-php/analytics": "0.2.*",
|
||||
"utopia-php/audit": "0.27.*",
|
||||
"appwrite/php-runtimes": "0.12.0",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "0.31.*",
|
||||
"utopia-php/analytics": "0.10.*",
|
||||
"utopia-php/audit": "0.33.*",
|
||||
"utopia-php/cache": "0.8.*",
|
||||
"utopia-php/cli": "0.13.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.36.*",
|
||||
"utopia-php/database": "0.43.*",
|
||||
"utopia-php/domains": "0.3.*",
|
||||
"utopia-php/framework": "0.28.*",
|
||||
"utopia-php/dsn": "0.1.*",
|
||||
"utopia-php/framework": "0.30.0",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/locale": "0.4.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/messaging": "0.1.*",
|
||||
"utopia-php/orchestration": "0.6.*",
|
||||
"utopia-php/orchestration": "0.9.*",
|
||||
"utopia-php/platform": "0.4.*",
|
||||
"utopia-php/pools": "0.4.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.5.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.14.*",
|
||||
"utopia-php/swoole": "0.5.*",
|
||||
"utopia-php/vcs": "0.3.*",
|
||||
"utopia-php/websocket": "0.1.*",
|
||||
"resque/php-resque": "1.3.6",
|
||||
"matomo/device-detector": "6.0.*",
|
||||
"dragonmantank/cron-expression": "3.3.1",
|
||||
"matomo/device-detector": "6.1.*",
|
||||
"dragonmantank/cron-expression": "3.3.2",
|
||||
"influxdb/influxdb-php": "1.15.2",
|
||||
"phpmailer/phpmailer": "6.6.0",
|
||||
"chillerlan/php-qrcode": "4.3.3",
|
||||
"phpmailer/phpmailer": "6.8.0",
|
||||
"chillerlan/php-qrcode": "4.3.4",
|
||||
"adhocore/jwt": "1.1.2",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"slickdeals/statsd": "3.1.0",
|
||||
"webonyx/graphql-php": "14.11.*"
|
||||
"league/csv": "9.7.1",
|
||||
"utopia-php/migration": "^0.3.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
|
@ -79,11 +86,11 @@
|
|||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"appwrite/sdk-generator": "0.33.*",
|
||||
"appwrite/sdk-generator": "0.34.*",
|
||||
"ext-fileinfo": "*",
|
||||
"phpunit/phpunit": "9.5.20",
|
||||
"squizlabs/php_codesniffer": "^3.6",
|
||||
"swoole/ide-helper": "4.8.9",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"swoole/ide-helper": "5.0.2",
|
||||
"textalk/websocket": "1.5.7"
|
||||
},
|
||||
"provide": {
|
||||
|
|
@ -94,4 +101,4 @@
|
|||
"php": "8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
880
composer.lock
generated
880
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@ version: '3'
|
|||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:2.7
|
||||
image: traefik:2.9
|
||||
<<: *x-logging
|
||||
container_name: appwrite-traefik
|
||||
command:
|
||||
|
|
@ -109,6 +109,7 @@ services:
|
|||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
|
|
@ -155,10 +156,8 @@ services:
|
|||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CONTAINERS
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
|
|
@ -177,6 +176,15 @@ services:
|
|||
- _APP_GRAPHQL_MAX_BATCH_SIZE
|
||||
- _APP_GRAPHQL_MAX_COMPLEXITY
|
||||
- _APP_GRAPHQL_MAX_DEPTH
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_VCS_GITHUB_WEBHOOK_SECRET
|
||||
- _APP_VCS_GITHUB_CLIENT_SECRET
|
||||
- _APP_VCS_GITHUB_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
|
|
@ -216,6 +224,8 @@ services:
|
|||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
|
|
@ -240,6 +250,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -269,6 +280,7 @@ services:
|
|||
- request-catcher
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
|
|
@ -298,6 +310,7 @@ services:
|
|||
- ./src:/usr/src/code/src
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -349,6 +362,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -369,7 +383,9 @@ services:
|
|||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
|
|
@ -377,6 +393,7 @@ services:
|
|||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
|
|
@ -391,6 +408,15 @@ services:
|
|||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_DOMAIN
|
||||
|
||||
appwrite-worker-certificates:
|
||||
entrypoint: worker-certificates
|
||||
|
|
@ -409,9 +435,11 @@ services:
|
|||
- ./src:/usr/src/code/src
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -438,9 +466,10 @@ services:
|
|||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- appwrite-executor
|
||||
- openruntimes-executor
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -452,73 +481,16 @@ services:
|
|||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_USAGE_STATS
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
|
||||
appwrite-executor:
|
||||
container_name: appwrite-executor
|
||||
<<: *x-logging
|
||||
entrypoint: executor
|
||||
stop_signal: SIGINT
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
appwrite:
|
||||
runtimes:
|
||||
ports:
|
||||
- 9519:80
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/src/code/dev
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- /tmp:/tmp:rw
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
- appwrite
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_VERSION
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CONTAINERS
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- OPEN_RUNTIMES_NETWORK
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_DOCKER_HUB_USERNAME
|
||||
- _APP_DOCKER_HUB_PASSWORD
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- DOCKERHUB_PULL_USERNAME
|
||||
- DOCKERHUB_PULL_PASSWORD
|
||||
- _APP_LOGGING_PROVIDER
|
||||
|
||||
appwrite-worker-mails:
|
||||
entrypoint: worker-mails
|
||||
|
|
@ -536,6 +508,7 @@ services:
|
|||
# - smtp
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
|
|
@ -566,6 +539,7 @@ services:
|
|||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
|
|
@ -575,6 +549,41 @@ services:
|
|||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-migrations:
|
||||
entrypoint: worker-migrations
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-migrations
|
||||
restart: unless-stopped
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./tests:/usr/src/code/tests
|
||||
depends_on:
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
|
||||
appwrite-maintenance:
|
||||
entrypoint: maintenance
|
||||
<<: *x-logging
|
||||
|
|
@ -585,13 +594,14 @@ services:
|
|||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
- _APP_DOMAIN_FUNCTIONS
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
|
|
@ -608,6 +618,7 @@ services:
|
|||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
|
||||
appwrite-usage:
|
||||
entrypoint: usage
|
||||
|
|
@ -619,11 +630,13 @@ services:
|
|||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/local/dev
|
||||
depends_on:
|
||||
- influxdb
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
|
|
@ -637,6 +650,7 @@ services:
|
|||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
|
|
@ -651,13 +665,99 @@ services:
|
|||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
|
||||
appwrite-assistant:
|
||||
container_name: appwrite-assistant
|
||||
image: appwrite/assistant:0.2.0
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
hostname: executor
|
||||
<<: *x-logging
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.3.5
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
# Host mount nessessary to share files between executor and runtimes.
|
||||
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
|
||||
- /tmp:/tmp:rw
|
||||
environment:
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME
|
||||
- OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD
|
||||
- OPR_EXECUTOR_ENV=$_APP_ENV
|
||||
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES
|
||||
- OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
|
||||
- OPR_EXECUTOR_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER
|
||||
- OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
|
||||
- OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE
|
||||
- OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION
|
||||
- OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION
|
||||
- OPR_EXECUTOR_STORAGE_DO_SPACES_BUCKET=$_APP_STORAGE_DO_SPACES_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_ACCESS_KEY=$_APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_SECRET=$_APP_STORAGE_BACKBLAZE_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_REGION=$_APP_STORAGE_BACKBLAZE_REGION
|
||||
- OPR_EXECUTOR_STORAGE_BACKBLAZE_BUCKET=$_APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_ACCESS_KEY=$_APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_SECRET=$_APP_STORAGE_LINODE_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_REGION=$_APP_STORAGE_LINODE_REGION
|
||||
- OPR_EXECUTOR_STORAGE_LINODE_BUCKET=$_APP_STORAGE_LINODE_BUCKET
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_ACCESS_KEY=$_APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_SECRET=$_APP_STORAGE_WASABI_SECRET
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION
|
||||
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
|
||||
|
||||
openruntimes-proxy:
|
||||
container_name: openruntimes-proxy
|
||||
hostname: proxy
|
||||
<<: *x-logging
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/proxy:0.3.0
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
environment:
|
||||
- OPR_PROXY_WORKER_PER_CORE=$_APP_WORKER_PER_CORE
|
||||
- OPR_PROXY_ENV=$_APP_ENV
|
||||
- OPR_PROXY_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
|
||||
- OPR_PROXY_SECRET=$_APP_EXECUTOR_SECRET
|
||||
- OPR_PROXY_LOGGING_PROVIDER=$_APP_LOGGING_PROVIDER
|
||||
- OPR_PROXY_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
|
||||
- OPR_PROXY_ALGORITHM=random
|
||||
- OPR_PROXY_EXECUTORS=executor
|
||||
- OPR_PROXY_HEALTHCHECK_INTERVAL=10000
|
||||
- OPR_PROXY_MAX_TIMEOUT=600
|
||||
- OPR_PROXY_HEALTHCHECK=enabled
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
|
|
@ -688,7 +788,7 @@ services:
|
|||
# - RELAY_FROM_HOSTS=192.168.0.0/16 ; *.yourdomain.com
|
||||
# - SMARTHOST_HOST=smtp
|
||||
# - SMARTHOST_PORT=587
|
||||
|
||||
|
||||
redis:
|
||||
image: redis:7.0.4-alpine
|
||||
<<: *x-logging
|
||||
|
|
@ -728,12 +828,12 @@ services:
|
|||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
environment:
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
|
||||
# Dev Tools Start ------------------------------------------------------------------------------------------
|
||||
#
|
||||
#
|
||||
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
|
||||
#
|
||||
# Here is a description of the different tools and why are we using them:
|
||||
|
|
@ -838,8 +938,11 @@ services:
|
|||
|
||||
networks:
|
||||
gateway:
|
||||
name: gateway
|
||||
appwrite:
|
||||
name: appwrite
|
||||
runtimes:
|
||||
name: runtimes
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
|
|
@ -851,5 +954,4 @@ volumes:
|
|||
appwrite-builds:
|
||||
appwrite-influxdb:
|
||||
appwrite-config:
|
||||
appwrite-executor:
|
||||
# appwrite-chronograf:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import io.appwrite.Client;
|
||||
import io.appwrite.coroutines.CoroutineCallback;
|
||||
import io.appwrite.services.Account;
|
||||
|
||||
Client client = new Client(context)
|
||||
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
|
||||
.setProject("5df5acd0d48c2"); // Your project ID
|
||||
|
||||
Account account = new Account(client);
|
||||
|
||||
account.createAnonymousSession(new CoroutineCallback<>((result, error) -> {
|
||||
if (error != null)
|
||||
error.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d("Appwrite", result.toString());
|
||||
}));
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue