mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/refactor-usage-sn' into feat-isolation-modes
# Conflicts: # composer.json # composer.lock # src/Appwrite/Platform/Tasks/CalcTierStats.php # tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
This commit is contained in:
@@ -5,12 +5,13 @@ _APP_WORKER_PER_CORE=6
|
||||
_APP_CONSOLE_WHITELIST_ROOT=disabled
|
||||
_APP_CONSOLE_WHITELIST_EMAILS=
|
||||
_APP_CONSOLE_WHITELIST_IPS=
|
||||
_APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io
|
||||
_APP_SYSTEM_EMAIL_NAME=Appwrite
|
||||
_APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io
|
||||
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io
|
||||
_APP_SYSTEM_RESPONSE_FORMAT=
|
||||
_APP_OPTIONS_ABUSE=disabled
|
||||
_APP_OPTIONS_ROUTER_PROTECTION=disbled
|
||||
_APP_OPTIONS_ROUTER_PROTECTION=disabled
|
||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
@@ -51,10 +52,6 @@ _APP_STORAGE_WASABI_BUCKET=
|
||||
_APP_STORAGE_ANTIVIRUS=disabled
|
||||
_APP_STORAGE_ANTIVIRUS_HOST=clamav
|
||||
_APP_STORAGE_ANTIVIRUS_PORT=3310
|
||||
_APP_INFLUXDB_HOST=influxdb
|
||||
_APP_INFLUXDB_PORT=8086
|
||||
_APP_STATSD_HOST=telegraf
|
||||
_APP_STATSD_PORT=8125
|
||||
_APP_SMTP_HOST=maildev
|
||||
_APP_SMTP_PORT=1025
|
||||
_APP_SMTP_SECURE=
|
||||
@@ -62,6 +59,7 @@ _APP_SMTP_USERNAME=
|
||||
_APP_SMTP_PASSWORD=
|
||||
_APP_SMS_PROVIDER=sms://username:password@mock
|
||||
_APP_SMS_FROM=+123456789
|
||||
_APP_SMS_PROJECTS_DENY_LIST=
|
||||
_APP_STORAGE_LIMIT=30000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
@@ -76,11 +74,12 @@ _APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://proxy/v1
|
||||
_APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_DELAY=
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
_APP_MAINTENANCE_RETENTION_ABUSE=86400
|
||||
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=5
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=20
|
||||
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
|
||||
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
|
||||
_APP_USAGE_STATS=enabled
|
||||
|
||||
@@ -17,6 +17,12 @@ jobs:
|
||||
fetch-depth: 2
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -35,11 +41,11 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
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 }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -75,8 +75,32 @@ jobs:
|
||||
- name: Run Unit Tests
|
||||
run: docker compose exec appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_test:
|
||||
name: E2E Test
|
||||
e2e_general_test:
|
||||
name: E2E General Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
- name: Run General Tests
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug
|
||||
|
||||
e2e_service_test:
|
||||
name: E2E Service Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
strategy:
|
||||
@@ -119,4 +143,4 @@ jobs:
|
||||
sleep 10
|
||||
|
||||
- name: Run ${{matrix.service}} Tests
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = 3.2.14
|
||||
branch = 3.2.16
|
||||
|
||||
+12
@@ -1,3 +1,14 @@
|
||||
# Version 1.4.14
|
||||
|
||||
## Changes
|
||||
- New usage metrics collection flow [#7005](https://github.com/appwrite/appwrite/pull/7005)
|
||||
- Deprecated influxdb, telegraf containers and removed all of their occurrences from the code.
|
||||
- Removed _APP_INFLUXDB_HOST, _APP_INFLUXDB_PORT, _APP_STATSD_HOST, _APP_STATSD_PORT env variables.
|
||||
- Removed usage labels dependency.
|
||||
- Dropped type attribute from stats collection.
|
||||
- Usage metrics are processed via new usage worker.
|
||||
- updated Metric names.
|
||||
|
||||
# Version 1.4.13
|
||||
|
||||
## Notable changes
|
||||
@@ -49,6 +60,7 @@
|
||||
* Use getQueueSize() in the Health service's get X queue endpoints [#7073](https://github.com/appwrite/appwrite/pull/7073)
|
||||
* Delete linked VCS repos and comments [#7066](https://github.com/appwrite/appwrite/pull/7066)
|
||||
|
||||
|
||||
# Version 1.4.9
|
||||
|
||||
## Bug fixes
|
||||
|
||||
@@ -210,7 +210,6 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi
|
||||
│ ├── Task
|
||||
│ ├── Template
|
||||
│ ├── URL
|
||||
│ ├── Usage
|
||||
│ └── Utopia
|
||||
└── tests # End to end & unit tests
|
||||
├── e2e
|
||||
|
||||
+10
-3
@@ -74,7 +74,6 @@ RUN mkdir -p /storage/uploads && \
|
||||
# Executables
|
||||
RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/maintenance && \
|
||||
chmod +x /usr/local/bin/usage && \
|
||||
chmod +x /usr/local/bin/install && \
|
||||
chmod +x /usr/local/bin/upgrade && \
|
||||
chmod +x /usr/local/bin/migrate && \
|
||||
@@ -85,6 +84,10 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/ssl && \
|
||||
chmod +x /usr/local/bin/test && \
|
||||
chmod +x /usr/local/bin/vars && \
|
||||
chmod +x /usr/local/bin/queue-retry && \
|
||||
chmod +x /usr/local/bin/queue-count-failed && \
|
||||
chmod +x /usr/local/bin/queue-count-processing && \
|
||||
chmod +x /usr/local/bin/queue-count-success && \
|
||||
chmod +x /usr/local/bin/worker-audits && \
|
||||
chmod +x /usr/local/bin/worker-certificates && \
|
||||
chmod +x /usr/local/bin/worker-databases && \
|
||||
@@ -95,7 +98,10 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-webhooks && \
|
||||
chmod +x /usr/local/bin/worker-migrations && \
|
||||
chmod +x /usr/local/bin/worker-hamster
|
||||
chmod +x /usr/local/bin/worker-hamster && \
|
||||
chmod +x /usr/local/bin/worker-usage && \
|
||||
chmod +x /usr/local/bin/worker-usage-dump
|
||||
|
||||
|
||||
# Cloud Executabless
|
||||
RUN chmod +x /usr/local/bin/hamster && \
|
||||
@@ -107,7 +113,8 @@ RUN chmod +x /usr/local/bin/hamster && \
|
||||
chmod +x /usr/local/bin/clear-card-cache && \
|
||||
chmod +x /usr/local/bin/calc-users-stats && \
|
||||
chmod +x /usr/local/bin/calc-tier-stats && \
|
||||
chmod +x /usr/local/bin/get-migration-stats
|
||||
chmod +x /usr/local/bin/get-migration-stats && \
|
||||
chmod +x /usr/local/bin/create-inf-metric
|
||||
|
||||
# Letsencrypt Permissions
|
||||
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
|
||||
|
||||
-24
@@ -125,30 +125,6 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
|
||||
};
|
||||
}, ['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('queue', function (Group $pools) {
|
||||
return $pools->get('queue')->pop()->getResource();
|
||||
}, ['pools']);
|
||||
|
||||
+62
-73
@@ -18,6 +18,63 @@ $auth = Config::getParam('auth', []);
|
||||
*/
|
||||
|
||||
$commonCollections = [
|
||||
'cache' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'cache',
|
||||
'name' => 'Cache',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'resource',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'accessedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'signature',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_accessedAt',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['accessedAt'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_resource',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resource'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'users' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('users'),
|
||||
@@ -1270,10 +1327,10 @@ $commonCollections = [
|
||||
]
|
||||
],
|
||||
|
||||
'stats' => [
|
||||
'stats_v2' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('stats'),
|
||||
'name' => 'Stats',
|
||||
'$id' => ID::custom('stats_v2'),
|
||||
'name' => 'stats_v2',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('metric'),
|
||||
@@ -1302,7 +1359,7 @@ $commonCollections = [
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 8,
|
||||
'signed' => false,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
@@ -1330,17 +1387,6 @@ $commonCollections = [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('type'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 1,
|
||||
'signed' => false,
|
||||
'required' => true,
|
||||
'default' => 0, // 0 -> count, 1 -> sum
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -1359,7 +1405,7 @@ $commonCollections = [
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_metric_period_time'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['metric', 'period', 'time'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_DESC],
|
||||
@@ -2883,63 +2929,6 @@ $projectCollections = array_merge([
|
||||
],
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'cache',
|
||||
'name' => 'Cache',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => 'resource',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'accessedAt',
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => 'signature',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 255,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => '_key_accessedAt',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['accessedAt'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_resource',
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resource'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'variables' => [
|
||||
'$collection' => Database::METADATA,
|
||||
'$id' => 'variables',
|
||||
|
||||
+26
-3
@@ -31,7 +31,7 @@ return [
|
||||
Exception::GENERAL_SERVICE_DISABLED => [
|
||||
'name' => Exception::GENERAL_SERVICE_DISABLED,
|
||||
'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.',
|
||||
'code' => 503,
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::GENERAL_UNAUTHORIZED_SCOPE => [
|
||||
'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE,
|
||||
@@ -103,6 +103,11 @@ return [
|
||||
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
|
||||
'code' => 405,
|
||||
],
|
||||
Exception::GENERAL_BAD_REQUEST => [
|
||||
'name' => Exception::GENERAL_BAD_REQUEST,
|
||||
'description' => 'There was an error processing your request. Please check the inputs and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** User Errors */
|
||||
Exception::USER_COUNT_EXCEEDED => [
|
||||
@@ -251,6 +256,11 @@ return [
|
||||
'description' => 'User phone is already verified',
|
||||
'code' => 409
|
||||
],
|
||||
Exception::USER_DELETION_PROHIBITED => [
|
||||
'name' => Exception::USER_DELETION_PROHIBITED,
|
||||
'description' => 'User deletion is not allowed for users with active memberships. Please delete all confirmed memberships before deleting the account.',
|
||||
'code' => 400
|
||||
],
|
||||
|
||||
/** Teams */
|
||||
Exception::TEAM_NOT_FOUND => [
|
||||
@@ -680,6 +690,7 @@ return [
|
||||
'name' => Exception::RULE_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
|
||||
'code' => 401,
|
||||
'publish' => true
|
||||
],
|
||||
Exception::PROJECT_SMTP_CONFIG_INVALID => [
|
||||
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
|
||||
@@ -767,10 +778,22 @@ return [
|
||||
],
|
||||
|
||||
/** Health */
|
||||
Exception::QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::QUEUE_SIZE_EXCEEDED,
|
||||
Exception::HEALTH_QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::HEALTH_QUEUE_SIZE_EXCEEDED,
|
||||
'description' => 'Queue size threshold hit.',
|
||||
'code' => 503,
|
||||
'publish' => false
|
||||
],
|
||||
|
||||
Exception::HEALTH_CERTIFICATE_EXPIRED => [
|
||||
'name' => Exception::HEALTH_CERTIFICATE_EXPIRED,
|
||||
'description' => 'The SSL certificate for the specified domain has expired and is no longer valid.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
Exception::HEALTH_INVALID_HOST => [
|
||||
'name' => Exception::HEALTH_INVALID_HOST,
|
||||
'description' => 'Failed to establish a connection to the specified domain. Please verify the domain name and ensure that the server is running and accessible.',
|
||||
'code' => 404,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<p>{{hello}}</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>{{hello}},</p>
|
||||
<p>{{body}}</p>
|
||||
|
||||
<a href="{{redirect}}" target="_blank">{{redirect}}</a>
|
||||
|
||||
<p><a href="{{redirect}}" target="_blank">{{redirect}}</a></p>
|
||||
<p>{{footer}}</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>{{thanks}}</p>
|
||||
|
||||
<p>{{signature}}</p>
|
||||
<p style="margin-bottom: 32px">
|
||||
{{thanks}},
|
||||
<br/>
|
||||
{{signature}}
|
||||
</p>
|
||||
@@ -3,30 +3,36 @@
|
||||
"settings.locale": "tr",
|
||||
"settings.direction": "ltr",
|
||||
"emails.sender": "%s Takımı",
|
||||
"emails.verification.subject": "",
|
||||
"emails.verification.hello": "",
|
||||
"emails.verification.body": "",
|
||||
"emails.verification.footer": "",
|
||||
"emails.verification.thanks": "",
|
||||
"emails.verification.signature": "",
|
||||
"emails.magicSession.subject": "",
|
||||
"emails.magicSession.hello": "",
|
||||
"emails.magicSession.body": "",
|
||||
"emails.magicSession.footer": "",
|
||||
"emails.magicSession.thanks": "",
|
||||
"emails.magicSession.signature": "",
|
||||
"emails.recovery.subject": "",
|
||||
"emails.recovery.hello": "",
|
||||
"emails.recovery.body": "",
|
||||
"emails.recovery.footer": "",
|
||||
"emails.recovery.thanks": "",
|
||||
"emails.recovery.signature": "",
|
||||
"emails.invitation.subject": "",
|
||||
"emails.invitation.hello": "",
|
||||
"emails.invitation.body": "",
|
||||
"emails.invitation.footer": "",
|
||||
"emails.invitation.thanks": "",
|
||||
"emails.invitation.signature": "",
|
||||
"emails.verification.subject": "Hesabını Doğrula",
|
||||
"emails.verification.hello": "Merhaba {{user}}",
|
||||
"emails.verification.body": "Eposta adresini doğrulamak için bu bağlantıyı kullanın.",
|
||||
"emails.verification.footer": "Eğer bu eposta adresini doğrulamak isteyen siz değilseniz devam etmeyin.",
|
||||
"emails.verification.thanks": "Teşekkürler",
|
||||
"emails.verification.signature": "{{project}} takımı",
|
||||
"emails.magicSession.subject": "Giriş",
|
||||
"emails.magicSession.hello": "Merhaba,",
|
||||
"emails.magicSession.body": "Giriş yapmak için tıklayın.",
|
||||
"emails.magicSession.footer": "Eğer bu eposta adresini kullanarak giriş yapmak istemediyseniz devam etmeyin.",
|
||||
"emails.magicSession.thanks": "Teşekkürler",
|
||||
"emails.magicSession.signature": "{{project}} takımı",
|
||||
"emails.recovery.subject": "Şifremi Sıfırla",
|
||||
"emails.recovery.hello": "Merhaba {{user}}",
|
||||
"emails.recovery.body": "{{project}} şifrenizi sıfırlamak için bu bağlantıyı kullanın.",
|
||||
"emails.recovery.footer": "Eğer şifre sıfırlama talebinde bulunmadıysanız devam etmeyin.",
|
||||
"emails.recovery.thanks": "Teşekkürler",
|
||||
"emails.recovery.signature": "{{project}} takımı",
|
||||
"emails.invitation.subject": "%s üzerinde %s Takımına Davet",
|
||||
"emails.invitation.hello": "Merhaba",
|
||||
"emails.invitation.body": "Bu epostayı aldınız, çünkü {{owner}} sizi {{project}} üzerinde {{team}} takımının üyesi olmaya davet etti.",
|
||||
"emails.invitation.footer": "Eğer ilgilenmiyorsanız devam etmeyin.",
|
||||
"emails.invitation.thanks": "Teşekkürler",
|
||||
"emails.invitation.signature": "{{project}} takımı",
|
||||
"emails.certificate.subject": "%s için sertifika hatası",
|
||||
"emails.certificate.hello": "Merhaba",
|
||||
"emails.certificate.body": "Alan adınız '{{domain}}' için sertifika oluşturulamadı. Deneme sayısı {{attempt}} ve hata sebebi: {{error}}",
|
||||
"emails.certificate.footer": "Geçmiş sertifikanız ilk denemeden sonra 30 gün daha geçerli kalacaktır. Bu konuyu araştırmanızı öneriyoruz, aksi taktirde alan adınız SSL sertifikasız kalacaktır.",
|
||||
"emails.certificate.thanks": "Teşekkürler",
|
||||
"emails.certificate.signature": "{{project}} takımı",
|
||||
"locale.country.unknown": "Bilinmeyen",
|
||||
"countries.af": "Afganistan",
|
||||
"countries.ao": "Angola",
|
||||
@@ -229,4 +235,4 @@
|
||||
"continents.na": "Kuzey Amerika",
|
||||
"continents.oc": "Okyanusya",
|
||||
"continents.sa": "Güney Amerika"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ return [
|
||||
[
|
||||
'key' => 'web',
|
||||
'name' => 'Console',
|
||||
'version' => '0.3.0',
|
||||
'version' => '0.5.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-console',
|
||||
'package' => '',
|
||||
'enabled' => true,
|
||||
@@ -195,8 +195,8 @@ return [
|
||||
'family' => APP_PLATFORM_CONSOLE,
|
||||
'prism' => 'javascript',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-console.git',
|
||||
'gitBranch' => 'dev',
|
||||
'gitUrl' => 'https://github.com/appwrite/sdk-for-console.git',
|
||||
'gitBranch' => 'main',
|
||||
'gitRepoName' => 'sdk-for-console',
|
||||
'gitUserName' => 'appwrite',
|
||||
],
|
||||
@@ -303,7 +303,7 @@ return [
|
||||
[
|
||||
'key' => 'ruby',
|
||||
'name' => 'Ruby',
|
||||
'version' => '10.1.0',
|
||||
'version' => '10.1.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-ruby',
|
||||
'package' => 'https://rubygems.org/gems/appwrite',
|
||||
'enabled' => true,
|
||||
|
||||
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
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
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
+21
-11
@@ -115,14 +115,6 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
// [
|
||||
// 'name' => '_APP_CONSOLE_WHITELIST_DOMAINS',
|
||||
// 'description' => 'This option allows you to limit creation of users to Appwrite console for users sharing the same email domains. This option is very useful for team working with company emails domain.\n\nTo enable this option, pass a list of allowed email domains separated by a comma.',
|
||||
// 'introduction' => '',
|
||||
// 'default' => '',
|
||||
// 'required' => false,
|
||||
// 'question' => '',
|
||||
// ],
|
||||
[
|
||||
'name' => '_APP_CONSOLE_WHITELIST_IPS',
|
||||
'description' => "This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\n\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma.",
|
||||
@@ -132,6 +124,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_CONSOLE_HOSTNAMES',
|
||||
'description' => 'This option allows you to add additional hostnames to your Appwrite console. This option is very useful for allowing access to the console project from additional domains. To enable it, pass a list of allowed hostnames separated by a comma.',
|
||||
'introduction' => '1.5.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_SYSTEM_EMAIL_NAME',
|
||||
'description' => 'This is the sender name value that will appear on email messages sent to developers from the Appwrite console. The default value is: \'Appwrite\'. You can use url encoded strings for spaces and special chars.',
|
||||
@@ -337,7 +338,7 @@ return [
|
||||
],
|
||||
[
|
||||
'category' => 'InfluxDB',
|
||||
'description' => 'Appwrite uses an InfluxDB server for managing time-series data and server stats. The InfluxDB env vars are used to allow Appwrite server to connect to the InfluxDB container.',
|
||||
'description' => 'Deprecated since 1.4.8.',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_INFLUXDB_HOST',
|
||||
@@ -361,7 +362,7 @@ return [
|
||||
],
|
||||
[
|
||||
'category' => 'StatsD',
|
||||
'description' => 'Appwrite uses a StatsD server for aggregating and sending stats data over a fast UDP connection. The StatsD env vars are used to allow Appwrite server to connect to the StatsD container.',
|
||||
'description' => 'Deprecated since 1.4.8.',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_STATSD_HOST',
|
||||
@@ -407,7 +408,7 @@ return [
|
||||
],
|
||||
[
|
||||
'name' => '_APP_SMTP_SECURE',
|
||||
'description' => 'SMTP secure connection protocol. Empty by default, change to \'tls\' if running on a secure connection.',
|
||||
'description' => 'SMTP secure connection protocol. Empty by default, change to \'tls\' or \'ssl\' if running on a secure connection.',
|
||||
'introduction' => '',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
@@ -947,6 +948,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_DELAY',
|
||||
'description' => 'Delay value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 0 seconds.',
|
||||
'introduction' => '1.5.0',
|
||||
'default' => '0',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
|
||||
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
|
||||
|
||||
+1
-1
Submodule app/console updated: 11244b1ad3...0a007a3b1b
+125
-68
@@ -45,6 +45,8 @@ use Utopia\Validator\WhiteList;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
|
||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||
$oauthDefaultFailure = '/auth/oauth2/failure';
|
||||
@@ -58,7 +60,6 @@ App::post('/v1/account')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'create')
|
||||
@@ -77,7 +78,8 @@ App::post('/v1/account')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -108,7 +110,7 @@ App::post('/v1/account')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
@@ -118,6 +120,8 @@ App::post('/v1/account')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
try {
|
||||
@@ -174,8 +178,6 @@ App::post('/v1/account/sessions/email')
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
->label('usage.params', ['provider:email'])
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createEmailSession')
|
||||
@@ -195,7 +197,8 @@ App::post('/v1/account/sessions/email')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
@@ -214,6 +217,8 @@ App::post('/v1/account/sessions/email')
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
@@ -428,8 +433,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->label('abuse-limit', 50)
|
||||
->label('abuse-key', 'ip:{ip}')
|
||||
->label('docs', false)
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
->label('usage.params', ['provider:{request.provider}'])
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
|
||||
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
|
||||
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
|
||||
@@ -550,11 +553,19 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
if (!$user->isEmpty()) {
|
||||
$userId = $user->getId();
|
||||
|
||||
$identitiesWithMatchingEmail = $dbForProject->find('identities', [
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
Query::notEqual('userId', $userId),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
if (!empty($identityWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$userWithMatchingEmail = $dbForProject->find('users', [
|
||||
Query::equal('email', [$email]),
|
||||
Query::notEqual('$id', $userId),
|
||||
]);
|
||||
if (!empty($userWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
@@ -626,7 +637,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -684,7 +695,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if (!empty($identitiesWithMatchingEmail)) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$dbForProject->createDocument('identities', new Document([
|
||||
@@ -794,7 +805,6 @@ App::get('/v1/account/identities')
|
||||
->desc('List Identities')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'listIdentities')
|
||||
@@ -842,14 +852,13 @@ App::get('/v1/account/identities')
|
||||
});
|
||||
|
||||
App::delete('/v1/account/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->desc('Delete identity')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('event', 'users.[userId].identities.[identityId].delete')
|
||||
->label('audits.event', 'identity.delete')
|
||||
->label('audits.resource', 'identity/{request.$identityId}')
|
||||
->label('audits.userId', '{user.$id}')
|
||||
->label('usage.metric', 'identities.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'deleteIdentity')
|
||||
@@ -859,7 +868,8 @@ App::delete('/v1/account/identities/:identityId')
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject) {
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
@@ -869,12 +879,17 @@ App::delete('/v1/account/identities/:identityId')
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $identity->getAttribute('userId'))
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/magic-url')
|
||||
->desc('Create magic URL session')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'magic-url')
|
||||
->label('audits.event', 'session.create')
|
||||
@@ -887,8 +902,8 @@ App::post('/v1/account/sessions/magic-url')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->label('abuse-limit', 60)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('userId', '', 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('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
|
||||
@@ -929,7 +944,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
@@ -1001,7 +1016,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.magicSession.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.magicSession.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks"))
|
||||
@@ -1056,11 +1071,11 @@ App::post('/v1/account/sessions/magic-url')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => '',
|
||||
'team' => '',
|
||||
'project' => $project->getAttribute('name'),
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $project->getAttribute('name')
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -1094,8 +1109,6 @@ App::put('/v1/account/sessions/magic-url')
|
||||
->label('audits.event', 'session.update')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
->label('usage.params', ['provider:magic-url'])
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateMagicURLSession')
|
||||
@@ -1210,7 +1223,7 @@ App::put('/v1/account/sessions/magic-url')
|
||||
|
||||
App::post('/v1/account/sessions/phone')
|
||||
->desc('Create phone session')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'phone')
|
||||
->label('audits.event', 'session.create')
|
||||
@@ -1224,7 +1237,7 @@ App::post('/v1/account/sessions/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},phone:{param-phone}')
|
||||
->label('abuse-key', ['url:{url},phone:{param-phone}', 'url:{url},ip:{ip}'])
|
||||
->param('userId', '', 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('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->inject('request')
|
||||
@@ -1326,6 +1339,7 @@ App::post('/v1/account/sessions/phone')
|
||||
$queueForMessaging
|
||||
->setRecipient($phone)
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
|
||||
$queueForEvents->setPayload(
|
||||
@@ -1469,8 +1483,6 @@ App::post('/v1/account/sessions/anonymous')
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
->label('usage.params', ['provider:anonymous'])
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createAnonymousSession')
|
||||
@@ -1648,7 +1660,6 @@ App::get('/v1/account')
|
||||
->desc('Get account')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'get')
|
||||
@@ -1669,7 +1680,6 @@ App::get('/v1/account/prefs')
|
||||
->desc('Get account preferences')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'getPrefs')
|
||||
@@ -1692,7 +1702,6 @@ App::get('/v1/account/sessions')
|
||||
->desc('List sessions')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'listSessions')
|
||||
@@ -1731,7 +1740,6 @@ App::get('/v1/account/logs')
|
||||
->desc('List logs')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'listLogs')
|
||||
@@ -1792,7 +1800,6 @@ App::get('/v1/account/sessions/:sessionId')
|
||||
->desc('Get session')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'getSession')
|
||||
@@ -1840,7 +1847,6 @@ App::patch('/v1/account/name')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateName')
|
||||
@@ -1875,7 +1881,6 @@ App::patch('/v1/account/password')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePassword')
|
||||
@@ -1893,7 +1898,8 @@ App::patch('/v1/account/password')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
// Check old password only if its an existing user.
|
||||
if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
|
||||
@@ -1920,6 +1926,8 @@ App::patch('/v1/account/password')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$user
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
@@ -1941,7 +1949,6 @@ App::patch('/v1/account/email')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateEmail')
|
||||
@@ -1958,7 +1965,9 @@ App::patch('/v1/account/email')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -1969,6 +1978,8 @@ App::patch('/v1/account/email')
|
||||
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$email = \strtolower($email);
|
||||
|
||||
// Makes sure this email is not already used in another identity
|
||||
@@ -1977,7 +1988,7 @@ App::patch('/v1/account/email')
|
||||
Query::notEqual('userId', $user->getId()),
|
||||
]);
|
||||
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$user
|
||||
@@ -1996,7 +2007,7 @@ App::patch('/v1/account/email')
|
||||
try {
|
||||
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
@@ -2011,7 +2022,6 @@ App::patch('/v1/account/phone')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePhone')
|
||||
@@ -2028,7 +2038,9 @@ App::patch('/v1/account/phone')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('project')
|
||||
->inject('hooks')
|
||||
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) {
|
||||
// passwordUpdate will be empty if the user has never set a password
|
||||
$passwordUpdate = $user->getAttribute('passwordUpdate');
|
||||
|
||||
@@ -2039,6 +2051,8 @@ App::patch('/v1/account/phone')
|
||||
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $phone)
|
||||
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
|
||||
@@ -2070,7 +2084,6 @@ App::patch('/v1/account/prefs')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePrefs')
|
||||
@@ -2104,7 +2117,6 @@ App::patch('/v1/account/status')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateStatus')
|
||||
@@ -2148,7 +2160,6 @@ App::delete('/v1/account/sessions/:sessionId')
|
||||
->label('event', 'users.[userId].sessions.[sessionId].delete')
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'deleteSession')
|
||||
@@ -2225,7 +2236,6 @@ App::patch('/v1/account/sessions/:sessionId')
|
||||
->label('audits.event', 'session.update')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateSession')
|
||||
@@ -2310,7 +2320,6 @@ App::delete('/v1/account/sessions')
|
||||
->label('event', 'users.[userId].sessions.[sessionId].delete')
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'deleteSessions')
|
||||
@@ -2372,7 +2381,6 @@ App::post('/v1/account/recovery')
|
||||
->label('audits.event', 'recovery.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createRecovery')
|
||||
@@ -2381,7 +2389,7 @@ App::post('/v1/account/recovery')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'ip:{ip}'])
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients'])
|
||||
->inject('request')
|
||||
@@ -2454,7 +2462,7 @@ App::post('/v1/account/recovery')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.recovery.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.recovery.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.recovery.thanks"))
|
||||
@@ -2509,16 +2517,16 @@ App::post('/v1/account/recovery')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $profile->getAttribute('name'),
|
||||
'team' => '',
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
->setRecipient($profile->getAttribute('email', ''))
|
||||
->setName($profile->getAttribute('name'))
|
||||
->setName($profile->getAttribute('name', ''))
|
||||
->setBody($body)
|
||||
->setVariables($emailVariables)
|
||||
->setSubject($subject)
|
||||
@@ -2550,7 +2558,6 @@ App::put('/v1/account/recovery')
|
||||
->label('audits.event', 'recovery.update')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateRecovery')
|
||||
@@ -2569,7 +2576,8 @@ App::put('/v1/account/recovery')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) {
|
||||
if ($password !== $passwordAgain) {
|
||||
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
|
||||
}
|
||||
@@ -2603,6 +2611,8 @@ App::put('/v1/account/recovery')
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
@@ -2637,7 +2647,6 @@ App::post('/v1/account/verification')
|
||||
->label('event', 'users.[userId].verification.[tokenId].create')
|
||||
->label('audits.event', 'verification.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createVerification')
|
||||
@@ -2705,7 +2714,7 @@ App::post('/v1/account/verification')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.verification.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.verification.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.verification.thanks"))
|
||||
@@ -2760,11 +2769,11 @@ App::post('/v1/account/verification')
|
||||
|
||||
$emailVariables = [
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $user->getAttribute('name'),
|
||||
'team' => '',
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -2798,7 +2807,6 @@ App::put('/v1/account/verification')
|
||||
->label('event', 'users.[userId].verification.[tokenId].update')
|
||||
->label('audits.event', 'verification.update')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateVerification')
|
||||
@@ -2854,12 +2862,12 @@ App::put('/v1/account/verification')
|
||||
|
||||
App::post('/v1/account/verification/phone')
|
||||
->desc('Create phone verification')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'account')
|
||||
->label('auth.type', 'phone')
|
||||
->label('event', 'users.[userId].verification.[tokenId].create')
|
||||
->label('audits.event', 'verification.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createPhoneVerification')
|
||||
@@ -2868,7 +2876,7 @@ App::post('/v1/account/verification/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'userId:{userId}')
|
||||
->label('abuse-key', ['url:{url},userId:{userId}', 'url:{url},ip:{ip}'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
@@ -2932,6 +2940,7 @@ App::post('/v1/account/verification/phone')
|
||||
$queueForMessaging
|
||||
->setRecipient($user->getAttribute('phone'))
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger()
|
||||
;
|
||||
|
||||
@@ -2959,7 +2968,6 @@ App::put('/v1/account/verification/phone')
|
||||
->label('event', 'users.[userId].verification.[tokenId].update')
|
||||
->label('audits.event', 'verification.update')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePhoneVerification')
|
||||
@@ -3010,3 +3018,52 @@ App::put('/v1/account/verification/phone')
|
||||
|
||||
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::delete('/v1/account')
|
||||
->desc('Delete account')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].delete')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.delete')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/account/delete.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('users', $user->getId());
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($user);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setPayload($response->output($user, Response::MODEL_USER));
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
+198
-344
@@ -402,7 +402,6 @@ App::post('/v1/databases')
|
||||
->label('scope', 'databases.write')
|
||||
->label('audits.event', 'database.create')
|
||||
->label('audits.resource', 'database/{response.$id}')
|
||||
->label('usage.metric', 'databases.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'create')
|
||||
@@ -476,7 +475,6 @@ App::get('/v1/databases')
|
||||
->desc('List databases')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'databases.read')
|
||||
->label('usage.metric', 'databases.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'list')
|
||||
@@ -524,7 +522,6 @@ App::get('/v1/databases/:databaseId')
|
||||
->desc('Get database')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'databases.read')
|
||||
->label('usage.metric', 'databases.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'get')
|
||||
@@ -639,7 +636,6 @@ App::put('/v1/databases/:databaseId')
|
||||
->label('event', 'databases.[databaseId].update')
|
||||
->label('audits.event', 'database.update')
|
||||
->label('audits.resource', 'database/{response.$id}')
|
||||
->label('usage.metric', 'databases.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'update')
|
||||
@@ -684,7 +680,6 @@ App::delete('/v1/databases/:databaseId')
|
||||
->label('event', 'databases.[databaseId].delete')
|
||||
->label('audits.event', 'database.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}')
|
||||
->label('usage.metric', 'databases.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'delete')
|
||||
@@ -729,8 +724,6 @@ App::post('/v1/databases/:databaseId/collections')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'collection.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.create')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'createCollection')
|
||||
@@ -796,8 +789,6 @@ App::get('/v1/databases/:databaseId/collections')
|
||||
->desc('List collections')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listCollections')
|
||||
@@ -855,8 +846,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->desc('Get collection')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'getCollection')
|
||||
@@ -891,8 +880,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
|
||||
->desc('List collection logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listCollectionLogs')
|
||||
@@ -990,8 +977,6 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].update')
|
||||
->label('audits.event', 'collection.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateCollection')
|
||||
@@ -1060,8 +1045,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].delete')
|
||||
->label('audits.event', 'collection.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.delete')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'deleteCollection')
|
||||
@@ -1117,8 +1100,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'createStringAttribute')
|
||||
@@ -1175,8 +1156,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createEmailAttribute')
|
||||
@@ -1219,8 +1198,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createEnumAttribute')
|
||||
@@ -1268,8 +1245,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createIpAttribute')
|
||||
@@ -1312,8 +1287,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createUrlAttribute')
|
||||
@@ -1356,8 +1329,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createIntegerAttribute')
|
||||
@@ -1429,8 +1400,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createFloatAttribute')
|
||||
@@ -1505,8 +1474,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createBooleanAttribute')
|
||||
@@ -1548,8 +1515,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createDatetimeAttribute')
|
||||
@@ -1594,8 +1559,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'attribute.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createRelationshipAttribute')
|
||||
@@ -1673,8 +1636,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
|
||||
->desc('List attributes')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listAttributes')
|
||||
@@ -1748,8 +1709,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
|
||||
->desc('Get attribute')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'getAttribute')
|
||||
@@ -1827,8 +1786,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateStringAttribute')
|
||||
@@ -1868,8 +1825,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/email
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateEmailAttribute')
|
||||
@@ -1909,8 +1864,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateEnumAttribute')
|
||||
@@ -1952,8 +1905,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:k
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateIpAttribute')
|
||||
@@ -1993,8 +1944,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateUrlAttribute')
|
||||
@@ -2034,8 +1983,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateIntegerAttribute')
|
||||
@@ -2085,8 +2032,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateFloatAttribute')
|
||||
@@ -2136,8 +2081,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/boole
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateBooleanAttribute')
|
||||
@@ -2176,8 +2119,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'documents.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateDatetimeAttribute')
|
||||
@@ -2216,8 +2157,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'documents.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'updateRelationshipAttribute')
|
||||
@@ -2272,8 +2211,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
|
||||
->label('audits.event', 'attribute.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'deleteAttribute')
|
||||
@@ -2383,8 +2320,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
->label('scope', 'collections.write')
|
||||
->label('audits.event', 'index.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'createIndex')
|
||||
@@ -2543,8 +2478,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
->desc('List indexes')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listIndexes')
|
||||
@@ -2608,8 +2541,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
|
||||
->desc('Get index')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('usage.metric', 'collections.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'getIndex')
|
||||
@@ -2652,8 +2583,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update')
|
||||
->label('audits.event', 'index.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'collections.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'deleteIndex')
|
||||
@@ -2718,8 +2647,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
->label('scope', 'documents.write')
|
||||
->label('audits.event', 'document.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('usage.metric', 'documents.{scope}.requests.create')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
@@ -2956,8 +2883,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
->desc('List documents')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('usage.metric', 'documents.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listDocuments')
|
||||
@@ -3072,6 +2997,33 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
$processDocument($collection, $document);
|
||||
}
|
||||
|
||||
$select = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT);
|
||||
}, false);
|
||||
|
||||
// Check if the SELECT query includes $databaseId and $collectionId
|
||||
$hasDatabaseId = false;
|
||||
$hasCollectionId = false;
|
||||
if ($select) {
|
||||
$hasDatabaseId = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$databaseId', $query->getValues()));
|
||||
}, false);
|
||||
$hasCollectionId = \array_reduce($queries, function ($result, $query) {
|
||||
return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$collectionId', $query->getValues()));
|
||||
}, false);
|
||||
}
|
||||
|
||||
if ($select) {
|
||||
foreach ($documents as $document) {
|
||||
if (!$hasDatabaseId) {
|
||||
$document->removeAttribute('$databaseId');
|
||||
}
|
||||
if (!$hasCollectionId) {
|
||||
$document->removeAttribute('$collectionId');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => $total,
|
||||
'documents' => $documents,
|
||||
@@ -3083,8 +3035,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
||||
->desc('Get document')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('usage.metric', 'documents.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'getDocument')
|
||||
@@ -3178,8 +3128,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
||||
->desc('List document logs')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.read')
|
||||
->label('usage.metric', 'documents.{scope}.requests.read')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'databases')
|
||||
->label('sdk.method', 'listDocumentLogs')
|
||||
@@ -3282,8 +3230,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
||||
->label('scope', 'documents.write')
|
||||
->label('audits.event', 'document.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
|
||||
->label('usage.metric', 'documents.{scope}.requests.update')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
@@ -3512,8 +3458,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
|
||||
->label('audits.event', 'document.delete')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
|
||||
->label('usage.metric', 'documents.{scope}.requests.delete')
|
||||
->label('usage.params', ['databaseId:{request.databaseId}', 'collectionId:{request.collectionId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
@@ -3630,112 +3574,72 @@ App::get('/v1/databases/usage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_DATABASES)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), '`Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
METRIC_DATABASES,
|
||||
METRIC_COLLECTIONS,
|
||||
METRIC_DOCUMENTS,
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
'databases.$all.count.total',
|
||||
'documents.$all.count.total',
|
||||
'collections.$all.count.total',
|
||||
'databases.$all.requests.create',
|
||||
'databases.$all.requests.read',
|
||||
'databases.$all.requests.update',
|
||||
'databases.$all.requests.delete',
|
||||
'collections.$all.requests.create',
|
||||
'collections.$all.requests.read',
|
||||
'collections.$all.requests.update',
|
||||
'collections.$all.requests.delete',
|
||||
'documents.$all.requests.create',
|
||||
'documents.$all.requests.read',
|
||||
'documents.$all.requests.update',
|
||||
'documents.$all.requests.delete'
|
||||
];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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--;
|
||||
}
|
||||
// Added 3'rd level to Index [period, metric, time] because of order by.
|
||||
$stats[$metric] = array_reverse($stats[$metric]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'databasesCount' => $stats['databases.$all.count.total'] ?? [],
|
||||
'documentsCount' => $stats['documents.$all.count.total'] ?? [],
|
||||
'collectionsCount' => $stats['collections.$all.count.total'] ?? [],
|
||||
'documentsCreate' => $stats['documents.$all.requests.create'] ?? [],
|
||||
'documentsRead' => $stats['documents.$all.requests.read'] ?? [],
|
||||
'documentsUpdate' => $stats['documents.$all.requests.update'] ?? [],
|
||||
'documentsDelete' => $stats['documents.$all.requests.delete'] ?? [],
|
||||
'collectionsCreate' => $stats['collections.$all.requests.create'] ?? [],
|
||||
'collectionsRead' => $stats['collections.$all.requests.read'] ?? [],
|
||||
'collectionsUpdate' => $stats['collections.$all.requests.update'] ?? [],
|
||||
'collectionsDelete' => $stats['collections.$all.requests.delete'] ?? [],
|
||||
'databasesCreate' => $stats['databases.$all.requests.create'] ?? [],
|
||||
'databasesRead' => $stats['databases.$all.requests.read'] ?? [],
|
||||
'databasesUpdate' => $stats['databases.$all.requests.update'] ?? [],
|
||||
'databasesDelete' => $stats['databases.$all.requests.delete'] ?? [],
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_DATABASES);
|
||||
}
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'databasesTotal' => $usage[$metrics[0]]['total'],
|
||||
'collectionsTotal' => $usage[$metrics[1]]['total'],
|
||||
'documentsTotal' => $usage[$metrics[2]]['total'],
|
||||
'databases' => $usage[$metrics[0]]['data'],
|
||||
'collections' => $usage[$metrics[1]]['data'],
|
||||
'documents' => $usage[$metrics[2]]['data'],
|
||||
]), Response::MODEL_USAGE_DATABASES);
|
||||
});
|
||||
|
||||
App::get('/v1/databases/:databaseId/usage')
|
||||
@@ -3749,102 +3653,76 @@ App::get('/v1/databases/:databaseId/usage')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_DATABASE)
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), '`Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), '`Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $databaseId, 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,
|
||||
],
|
||||
];
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
|
||||
$metrics = [
|
||||
'collections.' . $databaseId . '.count.total',
|
||||
'collections.' . $databaseId . '.requests.create',
|
||||
'collections.' . $databaseId . '.requests.read',
|
||||
'collections.' . $databaseId . '.requests.update',
|
||||
'collections.' . $databaseId . '.requests.delete',
|
||||
'documents.' . $databaseId . '.count.total',
|
||||
'documents.' . $databaseId . '.requests.create',
|
||||
'documents.' . $databaseId . '.requests.read',
|
||||
'documents.' . $databaseId . '.requests.update',
|
||||
'documents.' . $databaseId . '.requests.delete'
|
||||
];
|
||||
|
||||
$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--;
|
||||
}
|
||||
// TODO@kodumbeats explore performance if query is ordered by time ASC
|
||||
$stats[$metric] = array_reverse($stats[$metric]);
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'collectionsCount' => $stats["collections.{$databaseId}.count.total"] ?? [],
|
||||
'collectionsCreate' => $stats["collections.{$databaseId}.requests.create"] ?? [],
|
||||
'collectionsRead' => $stats["collections.{$databaseId}.requests.read"] ?? [],
|
||||
'collectionsUpdate' => $stats["collections.{$databaseId}.requests.update"] ?? [],
|
||||
'collectionsDelete' => $stats["collections.{$databaseId}.requests.delete"] ?? [],
|
||||
'documentsCount' => $stats["documents.{$databaseId}.count.total"] ?? [],
|
||||
'documentsCreate' => $stats["documents.{$databaseId}.requests.create"] ?? [],
|
||||
'documentsRead' => $stats["documents.{$databaseId}.requests.read"] ?? [],
|
||||
'documentsUpdate' => $stats["documents.{$databaseId}.requests.update"] ?? [],
|
||||
'documentsDelete' => $stats["documents.{$databaseId}.requests.delete"] ?? [],
|
||||
]);
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_DATABASE);
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS),
|
||||
str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS),
|
||||
];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'collectionsTotal' => $usage[$metrics[0]]['total'],
|
||||
'documentsTotal' => $usage[$metrics[1]]['total'],
|
||||
'collections' => $usage[$metrics[0]]['data'],
|
||||
'documents' => $usage[$metrics[1]]['data'],
|
||||
]), Response::MODEL_USAGE_DATABASE);
|
||||
});
|
||||
|
||||
App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
@@ -3859,7 +3737,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_COLLECTION)
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -3873,84 +3751,60 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collectionDocument->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS),
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
"documents.{$databaseId}/{$collectionId}.count.total",
|
||||
"documents.{$databaseId}/{$collectionId}.requests.create",
|
||||
"documents.{$databaseId}/{$collectionId}.requests.read",
|
||||
"documents.{$databaseId}/{$collectionId}.requests.update",
|
||||
"documents.{$databaseId}/{$collectionId}.requests.delete",
|
||||
];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'documentsCount' => $stats["documents.{$databaseId}/{$collectionId}.count.total"] ?? [],
|
||||
'documentsCreate' => $stats["documents.{$databaseId}/{$collectionId}.requests.create"] ?? [],
|
||||
'documentsRead' => $stats["documents.{$databaseId}/{$collectionId}.requests.read"] ?? [],
|
||||
'documentsUpdate' => $stats["documents.{$databaseId}/{$collectionId}.requests.update"] ?? [],
|
||||
'documentsDelete' => $stats["documents.{$databaseId}/{$collectionId}.requests.delete" ?? []]
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_COLLECTION);
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'documentsTotal' => $usage[$metrics[0]]['total'],
|
||||
'documents' => $usage[$metrics[0]]['data'],
|
||||
]), Response::MODEL_USAGE_COLLECTION);
|
||||
});
|
||||
|
||||
+153
-176
@@ -6,13 +6,13 @@ use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Event\Validator\FunctionEvent;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
@@ -456,9 +456,9 @@ App::get('/v1/functions/:functionId/usage')
|
||||
->label('sdk.method', 'getFunctionUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTION)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $functionId, string $range, Response $response, Database $dbForProject) {
|
||||
@@ -469,97 +469,85 @@ App::get('/v1/functions/:functionId/usage')
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS),
|
||||
str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE),
|
||||
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS),
|
||||
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE),
|
||||
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE),
|
||||
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS),
|
||||
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE),
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
"executions.$functionId.compute.total",
|
||||
"executions.$functionId.compute.success",
|
||||
"executions.$functionId.compute.failure",
|
||||
"executions.$functionId.compute.time",
|
||||
"builds.$functionId.compute.total",
|
||||
"builds.$functionId.compute.success",
|
||||
"builds.$functionId.compute.failure",
|
||||
"builds.$functionId.compute.time",
|
||||
];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'executionsTotal' => $stats["executions.$functionId.compute.total"] ?? [],
|
||||
'executionsFailure' => $stats["executions.$functionId.compute.failure"] ?? [],
|
||||
'executionsSuccess' => $stats["executions.$functionId.compute.success"] ?? [],
|
||||
'executionsTime' => $stats["executions.$functionId.compute.time"] ?? [],
|
||||
'buildsTotal' => $stats["builds.$functionId.compute.total"] ?? [],
|
||||
'buildsFailure' => $stats["builds.$functionId.compute.failure"] ?? [],
|
||||
'buildsSuccess' => $stats["builds.$functionId.compute.success"] ?? [],
|
||||
'buildsTime' => $stats["builds.$functionId.compute.time" ?? []]
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTION);
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'deploymentsTotal' => $usage[$metrics[0]]['total'],
|
||||
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
|
||||
'buildsTotal' => $usage[$metrics[2]]['total'],
|
||||
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
|
||||
'buildsTimeTotal' => $usage[$metrics[4]]['total'],
|
||||
'executionsTotal' => $usage[$metrics[5]]['total'],
|
||||
'executionsTimeTotal' => $usage[$metrics[6]]['total'],
|
||||
'deployments' => $usage[$metrics[0]]['data'],
|
||||
'deploymentsStorage' => $usage[$metrics[1]]['data'],
|
||||
'builds' => $usage[$metrics[2]]['data'],
|
||||
'buildsStorage' => $usage[$metrics[3]]['data'],
|
||||
'buildsTime' => $usage[$metrics[4]]['data'],
|
||||
'executions' => $usage[$metrics[5]]['data'],
|
||||
'executionsTime' => $usage[$metrics[6]]['data'],
|
||||
]), Response::MODEL_USAGE_FUNCTION);
|
||||
});
|
||||
|
||||
App::get('/v1/functions/usage')
|
||||
->desc('Get functions usage')
|
||||
->groups(['api', 'functions', 'usage'])
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'functions')
|
||||
@@ -567,97 +555,87 @@ App::get('/v1/functions/usage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), '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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
METRIC_FUNCTIONS,
|
||||
METRIC_DEPLOYMENTS,
|
||||
METRIC_DEPLOYMENTS_STORAGE,
|
||||
METRIC_BUILDS,
|
||||
METRIC_BUILDS_STORAGE,
|
||||
METRIC_BUILDS_COMPUTE,
|
||||
METRIC_EXECUTIONS,
|
||||
METRIC_EXECUTIONS_COMPUTE,
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
'executions.$all.compute.total',
|
||||
'executions.$all.compute.failure',
|
||||
'executions.$all.compute.success',
|
||||
'executions.$all.compute.time',
|
||||
'builds.$all.compute.total',
|
||||
'builds.$all.compute.failure',
|
||||
'builds.$all.compute.success',
|
||||
'builds.$all.compute.time',
|
||||
];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'executionsTotal' => $stats[$metrics[0]] ?? [],
|
||||
'executionsFailure' => $stats[$metrics[1]] ?? [],
|
||||
'executionsSuccess' => $stats[$metrics[2]] ?? [],
|
||||
'executionsTime' => $stats[$metrics[3]] ?? [],
|
||||
'buildsTotal' => $stats[$metrics[4]] ?? [],
|
||||
'buildsFailure' => $stats[$metrics[5]] ?? [],
|
||||
'buildsSuccess' => $stats[$metrics[6]] ?? [],
|
||||
'buildsTime' => $stats[$metrics[7]] ?? [],
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTIONS);
|
||||
}
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'functionsTotal' => $usage[$metrics[0]]['total'],
|
||||
'deploymentsTotal' => $usage[$metrics[1]]['total'],
|
||||
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
|
||||
'buildsTotal' => $usage[$metrics[3]]['total'],
|
||||
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
|
||||
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
|
||||
'executionsTotal' => $usage[$metrics[6]]['total'],
|
||||
'executionsTimeTotal' => $usage[$metrics[7]]['total'],
|
||||
'functions' => $usage[$metrics[0]]['data'],
|
||||
'deployments' => $usage[$metrics[1]]['data'],
|
||||
'deploymentsStorage' => $usage[$metrics[2]]['data'],
|
||||
'builds' => $usage[$metrics[3]]['data'],
|
||||
'buildsStorage' => $usage[$metrics[4]]['data'],
|
||||
'buildsTime' => $usage[$metrics[5]]['data'],
|
||||
'executions' => $usage[$metrics[6]]['data'],
|
||||
'executionsTime' => $usage[$metrics[7]]['data'],
|
||||
]), Response::MODEL_USAGE_FUNCTIONS);
|
||||
});
|
||||
|
||||
App::put('/v1/functions/:functionId')
|
||||
@@ -1520,11 +1498,11 @@ App::post('/v1/functions/:functionId/executions')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('usage')
|
||||
->inject('queueForUsage')
|
||||
->inject('mode')
|
||||
->inject('queueForFunctions')
|
||||
->inject('geodb')
|
||||
->action(function (string $functionId, string $body, bool $async, string $path, string $method, array $headers, Response $response, Document $project, Database $dbForProject, Document $user, Event $queueForEvents, Stats $usage, string $mode, Func $queueForFunctions, Reader $geodb) {
|
||||
->action(function (string $functionId, string $body, bool $async, string $path, string $method, array $headers, Response $response, Document $project, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode, Func $queueForFunctions, Reader $geodb) {
|
||||
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
@@ -1752,6 +1730,13 @@ App::post('/v1/functions/:functionId/executions')
|
||||
->setAttribute('responseStatusCode', 500)
|
||||
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
|
||||
Console::error($th->getMessage());
|
||||
} finally {
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
|
||||
;
|
||||
}
|
||||
|
||||
if ($function->getAttribute('logging')) {
|
||||
@@ -1759,14 +1744,6 @@ App::post('/v1/functions/:functionId/executions')
|
||||
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
|
||||
// TODO revise this later using route label
|
||||
$usage
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('executions.{scope}.compute', 1)
|
||||
->setParam('executionStatus', $execution->getAttribute('status', ''))
|
||||
->setParam('executionTime', $execution->getAttribute('duration')); // ms
|
||||
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
|
||||
+146
-10
@@ -7,6 +7,7 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
@@ -14,8 +15,11 @@ use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Validator\Domain;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::get('/v1/health')
|
||||
->desc('Get HTTP')
|
||||
@@ -355,7 +359,7 @@ App::get('/v1/health/queue/webhooks')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -382,12 +386,62 @@ App::get('/v1/health/queue/logs')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/certificate')
|
||||
->desc('Get the SSL certificate for a domain')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getCertificate')
|
||||
->label('sdk.description', '/docs/references/health/get-certificate.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_CERTIFICATE)
|
||||
->param('domain', null, new Multiple([new Domain(), new PublicDomain()]), Multiple::TYPE_STRING, 'Domain name')
|
||||
->inject('response')
|
||||
->action(function (string $domain, Response $response) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL)) {
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
}
|
||||
|
||||
$sslContext = stream_context_create([
|
||||
"ssl" => [
|
||||
"capture_peer_cert" => true
|
||||
]
|
||||
]);
|
||||
$sslSocket = stream_socket_client("ssl://" . $domain . ":443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext);
|
||||
if (!$sslSocket) {
|
||||
throw new Exception(Exception::HEALTH_INVALID_HOST);
|
||||
}
|
||||
|
||||
$streamContextParams = stream_context_get_params($sslSocket);
|
||||
$peerCertificate = $streamContextParams['options']['ssl']['peer_certificate'];
|
||||
$certificatePayload = openssl_x509_parse($peerCertificate);
|
||||
|
||||
|
||||
$sslExpiration = $certificatePayload['validTo_time_t'];
|
||||
$status = $sslExpiration < time() ? 'fail' : 'pass';
|
||||
|
||||
if ($status === 'fail') {
|
||||
throw new Exception(Exception::HEALTH_CERTIFICATE_EXPIRED);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'name' => $certificatePayload['name'],
|
||||
'subjectSN' => $certificatePayload['subject']['CN'],
|
||||
'issuerOrganisation' => $certificatePayload['issuer']['O'],
|
||||
'validFrom' => $certificatePayload['validFrom_time_t'],
|
||||
'validTo' => $certificatePayload['validTo_time_t'],
|
||||
'signatureTypeSN' => $certificatePayload['signatureTypeSN'],
|
||||
]), Response::MODEL_HEALTH_CERTIFICATE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/queue/certificates')
|
||||
->desc('Get certificates queue')
|
||||
->groups(['api', 'health'])
|
||||
@@ -409,7 +463,7 @@ App::get('/v1/health/queue/certificates')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -436,7 +490,7 @@ App::get('/v1/health/queue/builds')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -464,7 +518,7 @@ App::get('/v1/health/queue/databases')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -491,7 +545,7 @@ App::get('/v1/health/queue/deletes')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -518,7 +572,7 @@ App::get('/v1/health/queue/mails')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -545,7 +599,7 @@ App::get('/v1/health/queue/messaging')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -572,7 +626,7 @@ App::get('/v1/health/queue/migrations')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -599,7 +653,7 @@ App::get('/v1/health/queue/functions')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -648,6 +702,47 @@ App::get('/v1/health/storage/local')
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/storage')
|
||||
->desc('Get storage')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getStorage')
|
||||
->label('sdk.description', '/docs/references/health/get-storage.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('deviceFiles')
|
||||
->inject('deviceFunctions')
|
||||
->inject('deviceBuilds')
|
||||
->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) {
|
||||
$devices = [$deviceFiles, $deviceFunctions, $deviceBuilds];
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
foreach ($devices as $device) {
|
||||
if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if ($device->read($device->getPath('health.txt')) !== 'test') {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if (!$device->delete($device->getPath('health.txt'))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
|
||||
}
|
||||
}
|
||||
|
||||
$output = [
|
||||
'status' => 'pass',
|
||||
'ping' => \round((\microtime(true) - $checkStart) / 1000)
|
||||
];
|
||||
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/anti-virus')
|
||||
->desc('Get antivirus')
|
||||
->groups(['api', 'health'])
|
||||
@@ -687,6 +782,47 @@ App::get('/v1/health/anti-virus')
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/queue/failed/:name')
|
||||
->desc('Get number of failed queue jobs')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getFailedJobs')
|
||||
->param('name', '', new WhiteList([
|
||||
Event::DATABASE_QUEUE_NAME,
|
||||
Event::DELETE_QUEUE_NAME,
|
||||
Event::AUDITS_QUEUE_NAME,
|
||||
Event::MAILS_QUEUE_NAME,
|
||||
Event::FUNCTIONS_QUEUE_NAME,
|
||||
Event::USAGE_QUEUE_NAME,
|
||||
Event::WEBHOOK_CLASS_NAME,
|
||||
Event::CERTIFICATES_QUEUE_NAME,
|
||||
Event::BUILDS_QUEUE_NAME,
|
||||
Event::MESSAGING_QUEUE_NAME,
|
||||
Event::MIGRATIONS_QUEUE_NAME,
|
||||
Event::HAMSTER_CLASS_NAME
|
||||
]), 'The name of the queue')
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->label('sdk.description', '/docs/references/health/get-failed-queue-jobs.md')
|
||||
->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('response')
|
||||
->inject('queue')
|
||||
->action(function (string $name, int|string $threshold, Response $response, Connection $queue) {
|
||||
$threshold = \intval($threshold);
|
||||
|
||||
$client = new Client($name, $queue);
|
||||
$failed = $client->countFailedJobs();
|
||||
|
||||
if ($failed >= $threshold) {
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue failed jobs threshold hit. Current size is {$failed} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE);
|
||||
});
|
||||
|
||||
App::get('/v1/health/stats') // Currently only used internally
|
||||
->desc('Get system stats')
|
||||
->groups(['api', 'health'])
|
||||
|
||||
+144
-81
@@ -3,8 +3,10 @@
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Datetime as DateTimeValidator;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
@@ -14,11 +16,10 @@ 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'])
|
||||
->groups(['api', 'usage'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'project')
|
||||
@@ -26,100 +27,162 @@ App::get('/v1/project/usage')
|
||||
->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)
|
||||
->param('startDate', '', new DateTimeValidator(), 'Starting date for the usage')
|
||||
->param('endDate', '', new DateTimeValidator(), 'End date for the usage')
|
||||
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', 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,
|
||||
],
|
||||
];
|
||||
->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) {
|
||||
$stats = $total = $usage = [];
|
||||
$format = 'Y-m-d 00:00:00';
|
||||
$firstDay = (new DateTime($startDate))->format($format);
|
||||
$lastDay = (new DateTime($endDate))->format($format);
|
||||
|
||||
$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'
|
||||
];
|
||||
$metrics = [
|
||||
'total' => [
|
||||
METRIC_EXECUTIONS,
|
||||
METRIC_DOCUMENTS,
|
||||
METRIC_DATABASES,
|
||||
METRIC_USERS,
|
||||
METRIC_BUCKETS,
|
||||
METRIC_FILES_STORAGE
|
||||
],
|
||||
'period' => [
|
||||
METRIC_NETWORK_REQUESTS,
|
||||
METRIC_NETWORK_INBOUND,
|
||||
METRIC_NETWORK_OUTBOUND,
|
||||
METRIC_USERS,
|
||||
METRIC_EXECUTIONS
|
||||
]
|
||||
];
|
||||
|
||||
$stats = [];
|
||||
$factor = match ($period) {
|
||||
'1h' => 3600,
|
||||
'1d' => 86400,
|
||||
};
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$limit = $periods[$range]['limit'];
|
||||
$period = $periods[$range]['period'];
|
||||
$limit = match ($period) {
|
||||
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
|
||||
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
|
||||
};
|
||||
|
||||
$requestDocs = $dbForProject->find('stats', [
|
||||
Query::equal('period', [$period]),
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$format = match ($period) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
$stats[$metric] = [];
|
||||
foreach ($requestDocs as $requestDoc) {
|
||||
$stats[$metric][] = [
|
||||
'value' => $requestDoc->getAttribute('value'),
|
||||
'date' => $requestDoc->getAttribute('time'),
|
||||
];
|
||||
}
|
||||
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) {
|
||||
foreach ($metrics['total'] as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
$total[$metric] = $result['value'] ?? 0;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
foreach ($metrics['period'] as $metric) {
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::greaterThanEqual('time', $firstDay),
|
||||
Query::lessThan('time', $lastDay),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
|
||||
$stats[$metric] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$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]] ?? [],
|
||||
]);
|
||||
$now = time();
|
||||
foreach ($metrics['period'] as $metric) {
|
||||
$usage[$metric] = [];
|
||||
$leap = $now - ($limit * $factor);
|
||||
while ($leap < $now) {
|
||||
$leap += $factor;
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric][] = [
|
||||
'value' => $stats[$metric][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_PROJECT);
|
||||
$executionsBreakdown = array_map(function ($function) use ($dbForProject) {
|
||||
$id = $function->getId();
|
||||
$name = $function->getAttribute('name');
|
||||
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS);
|
||||
$value = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
return [
|
||||
'resourceId' => $id,
|
||||
'name' => $name,
|
||||
'value' => $value['value'] ?? 0,
|
||||
];
|
||||
}, $dbForProject->find('functions'));
|
||||
|
||||
$bucketsBreakdown = array_map(function ($bucket) use ($dbForProject) {
|
||||
$id = $bucket->getId();
|
||||
$name = $bucket->getAttribute('name');
|
||||
$metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE);
|
||||
$value = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
return [
|
||||
'resourceId' => $id,
|
||||
'name' => $name,
|
||||
'value' => $value['value'] ?? 0,
|
||||
];
|
||||
}, $dbForProject->find('buckets'));
|
||||
|
||||
// merge network inbound + outbound
|
||||
$projectBandwidth = [];
|
||||
foreach ($usage[METRIC_NETWORK_INBOUND] as $item) {
|
||||
$projectBandwidth[$item['date']] ??= 0;
|
||||
$projectBandwidth[$item['date']] += $item['value'];
|
||||
}
|
||||
|
||||
foreach ($usage[METRIC_NETWORK_OUTBOUND] as $item) {
|
||||
$projectBandwidth[$item['date']] ??= 0;
|
||||
$projectBandwidth[$item['date']] += $item['value'];
|
||||
}
|
||||
|
||||
|
||||
$network = [];
|
||||
foreach ($projectBandwidth as $date => $value) {
|
||||
$network[] = [
|
||||
'date' => $date,
|
||||
'value' => $value
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'requests' => ($usage[METRIC_NETWORK_REQUESTS]),
|
||||
'network' => $network,
|
||||
'users' => ($usage[METRIC_USERS]),
|
||||
'executions' => ($usage[METRIC_EXECUTIONS]),
|
||||
'executionsTotal' => $total[METRIC_EXECUTIONS],
|
||||
'documentsTotal' => $total[METRIC_DOCUMENTS],
|
||||
'databasesTotal' => $total[METRIC_DATABASES],
|
||||
'usersTotal' => $total[METRIC_USERS],
|
||||
'bucketsTotal' => $total[METRIC_BUCKETS],
|
||||
'filesStorageTotal' => $total[METRIC_FILES_STORAGE],
|
||||
'executionsBreakdown' => $executionsBreakdown,
|
||||
'bucketsBreakdown' => $bucketsBreakdown
|
||||
]), Response::MODEL_USAGE_PROJECT);
|
||||
});
|
||||
|
||||
// Variables
|
||||
|
||||
// Variables
|
||||
App::post('/v1/project/variables')
|
||||
->desc('Create Variable')
|
||||
->groups(['api'])
|
||||
|
||||
@@ -316,120 +316,6 @@ App::get('/v1/projects/:projectId')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::get('/v1/projects/:projectId/usage')
|
||||
->desc('Get usage stats for a project')
|
||||
->groups(['api', 'projects', 'usage'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->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('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->inject('dbForProject')
|
||||
->inject('register')
|
||||
->action(function (string $projectId, string $range, Response $response, Database $dbForConsole, Database $dbForProject, Registry $register) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$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,
|
||||
],
|
||||
];
|
||||
|
||||
$dbForProject->setNamespace("_{$project->getInternalId()}");
|
||||
|
||||
$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);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId')
|
||||
->desc('Update project')
|
||||
->groups(['api', 'projects'])
|
||||
@@ -1583,7 +1469,7 @@ App::patch('/v1/projects/:projectId/smtp')
|
||||
->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)
|
||||
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', true)
|
||||
->inject('response')
|
||||
->inject('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) {
|
||||
@@ -1727,7 +1613,7 @@ App::get('/v1/projects/:projectId/templates/email/:type/:locale')
|
||||
$message
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$type}.hello"))
|
||||
->setParam('{{footer}}', $localeObj->getText("emails.{$type}.footer"))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'))
|
||||
->setParam('{{body}}', $localeObj->getText('emails.' . $type . '.body'), escapeHtml: false)
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$type}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$type}.signature"))
|
||||
->setParam('{{direction}}', $localeObj->getText('settings.direction'));
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Validator\Domain as ValidatorDomain;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -278,7 +279,8 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole) {
|
||||
->inject('log')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
@@ -298,7 +300,14 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
$domain = new Domain($rule->getAttribute('domain', ''));
|
||||
|
||||
$validationStart = \microtime(true);
|
||||
if (!$validator->isValid($domain->get())) {
|
||||
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
|
||||
$log->addTag('dnsDomain', $domain->get());
|
||||
|
||||
$error = $validator->getLogs();
|
||||
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
|
||||
|
||||
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
||||
}
|
||||
|
||||
|
||||
+177
-217
@@ -44,6 +44,7 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\Storage\Compression\Compression;
|
||||
|
||||
App::post('/v1/storage/buckets')
|
||||
->desc('Create bucket')
|
||||
@@ -52,7 +53,6 @@ App::post('/v1/storage/buckets')
|
||||
->label('event', 'buckets.[bucketId].create')
|
||||
->label('audits.event', 'bucket.create')
|
||||
->label('audits.resource', 'bucket/{response.$id}')
|
||||
->label('usage.metric', 'buckets.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'createBucket')
|
||||
@@ -67,7 +67,7 @@ App::post('/v1/storage/buckets')
|
||||
->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('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::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)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->inject('response')
|
||||
@@ -148,7 +148,6 @@ App::get('/v1/storage/buckets')
|
||||
->desc('List buckets')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'buckets.read')
|
||||
->label('usage.metric', 'buckets.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'listBuckets')
|
||||
@@ -197,7 +196,6 @@ App::get('/v1/storage/buckets/:bucketId')
|
||||
->desc('Get bucket')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'buckets.read')
|
||||
->label('usage.metric', 'buckets.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getBucket')
|
||||
@@ -226,7 +224,6 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
->label('event', 'buckets.[bucketId].update')
|
||||
->label('audits.event', 'bucket.update')
|
||||
->label('audits.resource', 'bucket/{response.$id}')
|
||||
->label('usage.metric', 'buckets.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'updateBucket')
|
||||
@@ -241,7 +238,7 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
->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('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::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)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->inject('response')
|
||||
@@ -294,7 +291,6 @@ App::delete('/v1/storage/buckets/:bucketId')
|
||||
->label('audits.event', 'bucket.delete')
|
||||
->label('event', 'buckets.[bucketId].delete')
|
||||
->label('audits.resource', 'bucket/{request.bucketId}')
|
||||
->label('usage.metric', 'buckets.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'deleteBucket')
|
||||
@@ -337,9 +333,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
->label('audits.event', 'file.create')
|
||||
->label('event', 'buckets.[bucketId].files.[fileId].create')
|
||||
->label('audits.resource', 'file/{response.$id}')
|
||||
->label('usage.metric', 'files.{scope}.requests.create')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId},chunkId:{chunkId}')
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
@@ -538,19 +532,24 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
$fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption
|
||||
$data = '';
|
||||
// Compression
|
||||
$algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != COMPRESSION_TYPE_NONE) {
|
||||
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
|
||||
$data = $deviceFiles->read($path);
|
||||
switch ($algorithm) {
|
||||
case COMPRESSION_TYPE_ZSTD:
|
||||
case Compression::ZSTD:
|
||||
$compressor = new Zstd();
|
||||
break;
|
||||
case COMPRESSION_TYPE_GZIP:
|
||||
case Compression::GZIP:
|
||||
default:
|
||||
$compressor = new GZIP();
|
||||
break;
|
||||
}
|
||||
$data = $compressor->compress($data);
|
||||
} else {
|
||||
// reset the algorithm to none as we do not compress the file
|
||||
// if file size exceedes the APP_STORAGE_READ_BUFFER
|
||||
// regardless the bucket compression algoorithm
|
||||
$algorithm = Compression::NONE;
|
||||
}
|
||||
|
||||
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
|
||||
@@ -622,7 +621,17 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
->setAttribute('metadata', $metadata)
|
||||
->setAttribute('chunksUploaded', $chunksUploaded);
|
||||
|
||||
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
*/
|
||||
$validator = new Authorization(Database::PERMISSION_CREATE);
|
||||
if (!$validator->isValid($bucket->getCreate())) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
$file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
|
||||
}
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
@@ -659,7 +668,17 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
||||
->setAttribute('chunksUploaded', $chunksUploaded)
|
||||
->setAttribute('metadata', $metadata);
|
||||
|
||||
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
|
||||
/**
|
||||
* Validate create permission and skip authorization in updateDocument
|
||||
* Without this, the file creation will fail when user doesn't have update permission
|
||||
* However as with chunk upload even if we are updating, we are essentially creating a file
|
||||
* adding it's new chunk so we validate create permission instead of update
|
||||
*/
|
||||
$validator = new Authorization(Database::PERMISSION_CREATE);
|
||||
if (!$validator->isValid($bucket->getCreate())) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
$file = Authorization::skip(fn() => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
|
||||
}
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
@@ -689,8 +708,6 @@ App::get('/v1/storage/buckets/:bucketId/files')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('usage.metric', 'files.{scope}.requests.read')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'listFiles')
|
||||
->label('sdk.description', '/docs/references/storage/list-files.md')
|
||||
@@ -770,8 +787,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('usage.metric', 'files.{scope}.requests.read')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getFile')
|
||||
->label('sdk.description', '/docs/references/storage/get-file.md')
|
||||
@@ -821,8 +836,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
->label('cache', true)
|
||||
->label('cache.resourceType', 'bucket/{request.bucketId}')
|
||||
->label('cache.resource', 'file/{request.fileId}')
|
||||
->label('usage.metric', 'files.{scope}.requests.read')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getFilePreview')
|
||||
@@ -872,14 +885,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
|
||||
$output = 'jpg';
|
||||
}
|
||||
|
||||
$inputs = Config::getParam('storage-inputs');
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
|
||||
if ($fileSecurity && !$valid) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
} else {
|
||||
@@ -890,9 +895,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
|
||||
$output = 'jpg';
|
||||
}
|
||||
|
||||
$inputs = Config::getParam('storage-inputs');
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$fileLogos = Config::getParam('storage-logos');
|
||||
|
||||
$path = $file->getAttribute('path');
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
$algorithm = $file->getAttribute('algorithm', 'none');
|
||||
$algorithm = $file->getAttribute('algorithm', Compression::NONE);
|
||||
$cipher = $file->getAttribute('openSSLCipher');
|
||||
$mime = $file->getAttribute('mimeType');
|
||||
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
|
||||
@@ -903,7 +916,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
$path = $fileLogos['default_image'];
|
||||
}
|
||||
|
||||
$algorithm = 'none';
|
||||
$algorithm = Compression::NONE;
|
||||
$cipher = null;
|
||||
$background = (empty($background)) ? 'eceff1' : $background;
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
@@ -915,12 +928,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
}
|
||||
|
||||
if (empty($output)) {
|
||||
// when file extension is provided but it's not one of our
|
||||
// supported outputs we fallback to `jpg`
|
||||
if (!empty($type) && !array_key_exists($type, $outputs)) {
|
||||
$type = 'jpg';
|
||||
}
|
||||
|
||||
// when file extension is not provided and the mime type is not one of our supported outputs
|
||||
// we fallback to `jpg` output format
|
||||
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
|
||||
}
|
||||
|
||||
|
||||
$source = $deviceFiles->read($path);
|
||||
|
||||
if (!empty($cipher)) { // Decrypt
|
||||
@@ -935,11 +953,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
}
|
||||
|
||||
switch ($algorithm) {
|
||||
case 'zstd':
|
||||
case Compression::ZSTD:
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
$compressor = new GZIP();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
@@ -987,8 +1005,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
|
||||
->desc('Get file for download')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('usage.metric', 'files.{scope}.requests.read')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getFileDownload')
|
||||
@@ -1080,15 +1096,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
|
||||
);
|
||||
}
|
||||
|
||||
switch ($file->getAttribute('algorithm', 'none')) {
|
||||
case 'zstd':
|
||||
switch ($file->getAttribute('algorithm', Compression::NONE)) {
|
||||
case Compression::ZSTD:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
@@ -1129,8 +1145,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
->desc('Get file for view')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('usage.metric', 'files.{scope}.requests.read')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'storage')
|
||||
->label('sdk.method', 'getFileView')
|
||||
@@ -1231,15 +1245,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
);
|
||||
}
|
||||
|
||||
switch ($file->getAttribute('algorithm', 'none')) {
|
||||
case 'zstd':
|
||||
switch ($file->getAttribute('algorithm', Compression::NONE)) {
|
||||
case Compression::ZSTD:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
$compressor = new Zstd();
|
||||
$source = $compressor->decompress($source);
|
||||
break;
|
||||
case 'gzip':
|
||||
case Compression::GZIP:
|
||||
if (empty($source)) {
|
||||
$source = $deviceFiles->read($path);
|
||||
}
|
||||
@@ -1253,10 +1267,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
|
||||
$response->send(substr($source, $start, ($end - $start + 1)));
|
||||
}
|
||||
$response->send($source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($rangeHeader)) {
|
||||
$response->send($deviceFiles->read($path, $start, ($end - $start + 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
$size = $deviceFiles->getFileSize($path);
|
||||
@@ -1284,8 +1300,6 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->label('event', 'buckets.[bucketId].files.[fileId].update')
|
||||
->label('audits.event', 'file.update')
|
||||
->label('audits.resource', 'file/{response.$id}')
|
||||
->label('usage.metric', 'files.{scope}.requests.update')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
@@ -1394,8 +1408,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
->label('event', 'buckets.[bucketId].files.[fileId].delete')
|
||||
->label('audits.event', 'file.delete')
|
||||
->label('audits.resource', 'file/{request.fileId}')
|
||||
->label('usage.metric', 'files.{scope}.requests.delete')
|
||||
->label('usage.params', ['bucketId:{request.bucketId}'])
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
@@ -1487,7 +1499,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
||||
|
||||
App::get('/v1/storage/usage')
|
||||
->desc('Get usage stats for storage')
|
||||
->groups(['api', 'storage', 'usage'])
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'storage')
|
||||
@@ -1495,109 +1507,78 @@ App::get('/v1/storage/usage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_STORAGE)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
METRIC_BUCKETS,
|
||||
METRIC_FILES,
|
||||
METRIC_FILES_STORAGE,
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
'project.$all.storage.size',
|
||||
'buckets.$all.count.total',
|
||||
'buckets.$all.requests.create',
|
||||
'buckets.$all.requests.read',
|
||||
'buckets.$all.requests.update',
|
||||
'buckets.$all.requests.delete',
|
||||
'files.$all.storage.size',
|
||||
'files.$all.count.total',
|
||||
'files.$all.requests.create',
|
||||
'files.$all.requests.read',
|
||||
'files.$all.requests.update',
|
||||
'files.$all.requests.delete',
|
||||
];
|
||||
$total = [];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'bucketsCount' => $stats['buckets.$all.count.total'],
|
||||
'bucketsCreate' => $stats['buckets.$all.requests.create'],
|
||||
'bucketsRead' => $stats['buckets.$all.requests.read'],
|
||||
'bucketsUpdate' => $stats['buckets.$all.requests.update'],
|
||||
'bucketsDelete' => $stats['buckets.$all.requests.delete'],
|
||||
'storage' => $stats['project.$all.storage.size'],
|
||||
'filesCount' => $stats['files.$all.count.total'],
|
||||
'filesCreate' => $stats['files.$all.requests.create'],
|
||||
'filesRead' => $stats['files.$all.requests.read'],
|
||||
'filesUpdate' => $stats['files.$all.requests.update'],
|
||||
'filesDelete' => $stats['files.$all.requests.delete'],
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_STORAGE);
|
||||
}
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'bucketsTotal' => $usage[$metrics[0]]['total'],
|
||||
'filesTotal' => $usage[$metrics[1]]['total'],
|
||||
'filesStorageTotal' => $usage[$metrics[2]]['total'],
|
||||
'buckets' => $usage[$metrics[0]]['data'],
|
||||
'files' => $usage[$metrics[1]]['data'],
|
||||
'storage' => $usage[$metrics[2]]['data'],
|
||||
]), Response::MODEL_USAGE_STORAGE);
|
||||
});
|
||||
|
||||
App::get('/v1/storage/:bucketId/usage')
|
||||
->desc('Get usage stats for a storage bucket')
|
||||
->groups(['api', 'storage', 'usage'])
|
||||
->desc('Get usage stats for storage bucket')
|
||||
->groups(['api', 'storage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'storage')
|
||||
@@ -1606,7 +1587,7 @@ App::get('/v1/storage/:bucketId/usage')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_BUCKETS)
|
||||
->param('bucketId', '', new UID(), 'Bucket ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) {
|
||||
@@ -1617,86 +1598,65 @@ App::get('/v1/storage/:bucketId/usage')
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES),
|
||||
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE),
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
"files.{$bucketId}.count.total",
|
||||
"files.{$bucketId}.storage.size",
|
||||
"files.{$bucketId}.requests.create",
|
||||
"files.{$bucketId}.requests.read",
|
||||
"files.{$bucketId}.requests.update",
|
||||
"files.{$bucketId}.requests.delete",
|
||||
];
|
||||
|
||||
$stats = [];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'filesCount' => $stats[$metrics[0]],
|
||||
'filesStorage' => $stats[$metrics[1]],
|
||||
'filesCreate' => $stats[$metrics[2]],
|
||||
'filesRead' => $stats[$metrics[3]],
|
||||
'filesUpdate' => $stats[$metrics[4]],
|
||||
'filesDelete' => $stats[$metrics[5]],
|
||||
]);
|
||||
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_BUCKETS);
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'filesTotal' => $usage[$metrics[0]]['total'],
|
||||
'filesStorageTotal' => $usage[$metrics[1]]['total'],
|
||||
'files' => $usage[$metrics[0]]['data'],
|
||||
'storage' => $usage[$metrics[1]]['data'],
|
||||
]), Response::MODEL_USAGE_BUCKETS);
|
||||
});
|
||||
|
||||
@@ -533,8 +533,8 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
|
||||
}
|
||||
$team->setAttribute('total', $team->getAttribute('total', 0) + 1);
|
||||
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
|
||||
|
||||
Authorization::skip(fn() => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $invitee->getId());
|
||||
} else {
|
||||
@@ -556,7 +556,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
->setParam('{{body}}', $body)
|
||||
->setParam('{{body}}', $body, escapeHtml: false)
|
||||
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
|
||||
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
|
||||
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
|
||||
@@ -612,11 +612,11 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$emailVariables = [
|
||||
'owner' => $user->getAttribute('name'),
|
||||
'direction' => $locale->getText('settings.direction'),
|
||||
/* {{user}} ,{{team}}, {{project}} and {{redirect}} are required in the templates */
|
||||
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
|
||||
'user' => $user->getAttribute('name'),
|
||||
'team' => $team->getAttribute('name'),
|
||||
'project' => $projectName,
|
||||
'redirect' => $url
|
||||
'redirect' => $url,
|
||||
'project' => $projectName
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -641,6 +641,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
$queueForMessaging
|
||||
->setRecipient($phone)
|
||||
->setMessage($message)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
@@ -966,7 +967,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1)));
|
||||
Authorization::skip(fn() => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('teamId', $team->getId())
|
||||
@@ -1046,8 +1047,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
if ($membership->getAttribute('confirm')) { // Count only confirmed members
|
||||
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0));
|
||||
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
|
||||
Authorization::skip(fn() => $dbForProject->decreaseDocumentAttribute('teams', $team->getId(), 'total', 1, 0));
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
|
||||
+105
-136
@@ -9,6 +9,7 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Users;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
@@ -39,10 +40,12 @@ use Utopia\Validator\Integer;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
|
||||
/** 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 $queueForEvents): Document
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document
|
||||
{
|
||||
$plaintextPassword = $password;
|
||||
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
|
||||
@@ -65,13 +68,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, $phone);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
if (!$personalDataValidator->isValid($plaintextPassword)) {
|
||||
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([
|
||||
$user = new Document([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
@@ -97,7 +100,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
]));
|
||||
]);
|
||||
|
||||
if ($hash === 'plaintext') {
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
|
||||
}
|
||||
|
||||
$user = $dbForProject->createDocument('users', $user);
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -114,7 +123,6 @@ App::post('/v1/users')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'create')
|
||||
@@ -131,9 +139,9 @@ App::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents);
|
||||
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -146,7 +154,6 @@ App::post('/v1/users/bcrypt')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createBcryptUser')
|
||||
@@ -162,8 +169,9 @@ App::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -177,7 +185,6 @@ App::post('/v1/users/md5')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createMD5User')
|
||||
@@ -193,8 +200,9 @@ App::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -208,7 +216,6 @@ App::post('/v1/users/argon2')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createArgon2User')
|
||||
@@ -224,8 +231,9 @@ App::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -239,7 +247,6 @@ App::post('/v1/users/sha')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createSHAUser')
|
||||
@@ -256,14 +263,15 @@ App::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$options = '{}';
|
||||
|
||||
if (!empty($passwordVersion)) {
|
||||
$options = '{"version":"' . $passwordVersion . '"}';
|
||||
}
|
||||
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -277,7 +285,6 @@ App::post('/v1/users/phpass')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createPHPassUser')
|
||||
@@ -293,8 +300,9 @@ App::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -308,7 +316,6 @@ App::post('/v1/users/scrypt')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createScryptUser')
|
||||
@@ -329,7 +336,8 @@ App::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$options = [
|
||||
'salt' => $passwordSalt,
|
||||
'costCpu' => $passwordCpu,
|
||||
@@ -338,7 +346,7 @@ App::post('/v1/users/scrypt')
|
||||
'length' => $passwordLength
|
||||
];
|
||||
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -352,7 +360,6 @@ App::post('/v1/users/scrypt-modified')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.create')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createScryptModifiedUser')
|
||||
@@ -371,8 +378,9 @@ App::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents);
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -383,7 +391,6 @@ App::get('/v1/users')
|
||||
->desc('List users')
|
||||
->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', 'list')
|
||||
@@ -432,7 +439,6 @@ App::get('/v1/users/:userId')
|
||||
->desc('Get user')
|
||||
->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', 'get')
|
||||
@@ -458,7 +464,6 @@ App::get('/v1/users/:userId/prefs')
|
||||
->desc('Get user preferences')
|
||||
->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', 'getPrefs')
|
||||
@@ -486,7 +491,6 @@ App::get('/v1/users/:userId/sessions')
|
||||
->desc('List user sessions')
|
||||
->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', 'listSessions')
|
||||
@@ -528,7 +532,6 @@ App::get('/v1/users/:userId/memberships')
|
||||
->desc('List user memberships')
|
||||
->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', 'listMemberships')
|
||||
@@ -568,7 +571,6 @@ App::get('/v1/users/:userId/logs')
|
||||
->desc('List user logs')
|
||||
->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', 'listLogs')
|
||||
@@ -650,7 +652,6 @@ 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')
|
||||
@@ -703,7 +704,6 @@ App::patch('/v1/users/:userId/status')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateStatus')
|
||||
@@ -739,7 +739,6 @@ App::put('/v1/users/:userId/labels')
|
||||
->label('scope', 'users.write')
|
||||
->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', 'updateLabels')
|
||||
@@ -777,7 +776,6 @@ App::patch('/v1/users/:userId/verification/phone')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'verification.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', 'updatePhoneVerification')
|
||||
@@ -814,7 +812,6 @@ App::patch('/v1/users/:userId/name')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateName')
|
||||
@@ -852,7 +849,6 @@ App::patch('/v1/users/:userId/password')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updatePassword')
|
||||
@@ -866,7 +862,8 @@ App::patch('/v1/users/:userId/password')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
@@ -881,6 +878,8 @@ App::patch('/v1/users/:userId/password')
|
||||
}
|
||||
}
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
@@ -917,7 +916,6 @@ App::patch('/v1/users/:userId/email')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateEmail')
|
||||
@@ -973,7 +971,6 @@ App::patch('/v1/users/:userId/phone')
|
||||
->label('scope', 'users.write')
|
||||
->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', 'updatePhone')
|
||||
@@ -1018,7 +1015,6 @@ App::patch('/v1/users/:userId/verification')
|
||||
->label('audits.event', 'verification.update')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('audits.userId', '{request.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateEmailVerification')
|
||||
@@ -1051,7 +1047,6 @@ App::patch('/v1/users/:userId/prefs')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.prefs')
|
||||
->label('scope', 'users.write')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updatePrefs')
|
||||
@@ -1087,7 +1082,6 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'deleteSession')
|
||||
@@ -1131,7 +1125,6 @@ App::delete('/v1/users/:userId/sessions')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'deleteSessions')
|
||||
@@ -1174,7 +1167,6 @@ App::delete('/v1/users/:userId')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.delete')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.delete')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'delete')
|
||||
@@ -1211,13 +1203,12 @@ App::delete('/v1/users/:userId')
|
||||
});
|
||||
|
||||
App::delete('/v1/users/identities/:identityId')
|
||||
->desc('Delete Identity')
|
||||
->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')
|
||||
@@ -1227,7 +1218,8 @@ App::delete('/v1/users/identities/:identityId')
|
||||
->param('identityId', '', new UID(), 'Identity ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject) {
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$identity = $dbForProject->getDocument('identities', $identityId);
|
||||
|
||||
@@ -1237,12 +1229,17 @@ App::delete('/v1/users/identities/:identityId')
|
||||
|
||||
$dbForProject->deleteDocument('identities', $identityId);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $identity->getAttribute('userId'))
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::get('/v1/users/usage')
|
||||
->desc('Get usage stats for the users API')
|
||||
->groups(['api', 'users', 'usage'])
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'users')
|
||||
@@ -1250,97 +1247,69 @@ App::get('/v1/users/usage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
|
||||
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('register')
|
||||
->action(function (string $range, string $provider, Response $response, Database $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,
|
||||
],
|
||||
];
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
METRIC_USERS,
|
||||
METRIC_SESSIONS,
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
'users.$all.count.total',
|
||||
'users.$all.requests.create',
|
||||
'users.$all.requests.read',
|
||||
'users.$all.requests.update',
|
||||
'users.$all.requests.delete',
|
||||
'sessions.$all.requests.create',
|
||||
'sessions.$all.requests.delete',
|
||||
"sessions.$provider.requests.create",
|
||||
];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $count => $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$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]);
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'usersCount' => $stats['users.$all.count.total'] ?? [],
|
||||
'usersCreate' => $stats['users.$all.requests.create'] ?? [],
|
||||
'usersRead' => $stats['users.$all.requests.read'] ?? [],
|
||||
'usersUpdate' => $stats['users.$all.requests.update'] ?? [],
|
||||
'usersDelete' => $stats['users.$all.requests.delete'] ?? [],
|
||||
'sessionsCreate' => $stats['sessions.$all.requests.create'] ?? [],
|
||||
'sessionsProviderCreate' => $stats["sessions.$provider.requests.create"] ?? [],
|
||||
'sessionsDelete' => $stats['sessions.$all.requests.delete' ?? []]
|
||||
]);
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_USERS);
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'usersTotal' => $usage[$metrics[0]]['total'],
|
||||
'sessionsTotal' => $usage[$metrics[1]]['total'],
|
||||
'users' => $usage[$metrics[0]]['data'],
|
||||
'sessions' => $usage[$metrics[1]]['data'],
|
||||
]), Response::MODEL_USAGE_USERS);
|
||||
});
|
||||
|
||||
+271
-100
@@ -9,9 +9,8 @@ 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 MaxMind\Db\Reader;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\View;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
@@ -19,6 +18,8 @@ use Utopia\Config\Config;
|
||||
use Utopia\Domains\Domain;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
|
||||
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
|
||||
@@ -31,6 +32,7 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
|
||||
@@ -38,6 +40,7 @@ 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 Executor\Executor;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
@@ -45,7 +48,7 @@ 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)
|
||||
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb)
|
||||
{
|
||||
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
|
||||
|
||||
@@ -117,59 +120,218 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques
|
||||
$path .= '?' . $query;
|
||||
}
|
||||
|
||||
|
||||
$body = $swooleRequest->getContent() ?? '';
|
||||
$method = $swooleRequest->server['request_method'];
|
||||
|
||||
$requestHeaders = $request->getHeaders();
|
||||
|
||||
$body = \json_encode([
|
||||
'async' => false,
|
||||
'body' => $swooleRequest->getContent() ?? '',
|
||||
'method' => $swooleRequest->server['request_method'],
|
||||
'path' => $path,
|
||||
'headers' => $requestHeaders
|
||||
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
|
||||
if ($function->isEmpty() || !$function->getAttribute('enabled')) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
|
||||
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
|
||||
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
/** Check if build has completed */
|
||||
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
|
||||
if ($build->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($build->getAttribute('status') !== 'ready') {
|
||||
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
|
||||
}
|
||||
|
||||
$permissions = $function->getAttribute('execute');
|
||||
|
||||
if (!(\in_array('any', $permissions)) && (\in_array('guests', $permissions))) {
|
||||
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
|
||||
}
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
|
||||
$ip = $headers['x-real-ip'] ?? '';
|
||||
if (!empty($ip)) {
|
||||
$record = $geodb->get($ip);
|
||||
|
||||
if ($record) {
|
||||
$eu = Config::getParam('locale-eu');
|
||||
|
||||
$headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? '';
|
||||
$headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? '';
|
||||
$headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
|
||||
$headersFiltered = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
|
||||
$headersFiltered[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
$executionId = ID::unique();
|
||||
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => [],
|
||||
'functionInternalId' => $function->getInternalId(),
|
||||
'functionId' => $function->getId(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => 'http', // http / schedule / event
|
||||
'status' => 'processing', // waiting / processing / completed / failed
|
||||
'responseStatusCode' => 0,
|
||||
'responseHeaders' => [],
|
||||
'requestPath' => $path,
|
||||
'requestMethod' => $method,
|
||||
'requestHeaders' => $headersFiltered,
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
'search' => implode(' ', [$functionId, $executionId]),
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . \strlen($body),
|
||||
'X-Appwrite-Project: ' . $projectId
|
||||
];
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('executionId', $execution->getId())
|
||||
->setContext('function', $function);
|
||||
|
||||
$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);
|
||||
\curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
$executionResponse = \curl_exec($ch);
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = \curl_error($ch);
|
||||
$errNo = \curl_errno($ch);
|
||||
$vars = [];
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error);
|
||||
// V2 vars
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
}
|
||||
|
||||
if ($statusCode >= 400) {
|
||||
$error = \json_decode($executionResponse, true)['message'];
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error);
|
||||
// Shared vars
|
||||
foreach ($function->getAttribute('varsProject', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$execution = \json_decode($executionResponse, true);
|
||||
// Function vars
|
||||
foreach ($function->getAttribute('vars', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($execution['responseHeaders'] as $header) {
|
||||
if (\strtolower($header['name']) === 'content-type') {
|
||||
$contentType = $header['value'];
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_ID' => $functionId,
|
||||
'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'] ?? '',
|
||||
]);
|
||||
|
||||
/** Execute function */
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $function->getAttribute('timeout', 0),
|
||||
image: $runtime['image'],
|
||||
source: $build->getAttribute('path', ''),
|
||||
entrypoint: $deployment->getAttribute('entrypoint', ''),
|
||||
version: $version,
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $command,
|
||||
requestTimeout: 30
|
||||
);
|
||||
|
||||
$headersFiltered = [];
|
||||
foreach ($executionResponse['headers'] as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
|
||||
$headersFiltered[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
}
|
||||
|
||||
$response->setHeader($header['name'], $header['value']);
|
||||
/** Update execution status */
|
||||
$status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed';
|
||||
$execution->setAttribute('status', $status);
|
||||
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
|
||||
$execution->setAttribute('responseHeaders', $headersFiltered);
|
||||
$execution->setAttribute('logs', $executionResponse['logs']);
|
||||
$execution->setAttribute('errors', $executionResponse['errors']);
|
||||
$execution->setAttribute('duration', $executionResponse['duration']);
|
||||
} catch (\Throwable $th) {
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
$execution
|
||||
->setAttribute('duration', $durationEnd - $durationStart)
|
||||
->setAttribute('status', 'failed')
|
||||
->setAttribute('responseStatusCode', 500)
|
||||
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
|
||||
Console::error($th->getMessage());
|
||||
} finally {
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
|
||||
;
|
||||
}
|
||||
|
||||
if ($function->getAttribute('logging')) {
|
||||
/** @var Document $execution */
|
||||
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
|
||||
}
|
||||
|
||||
$execution->setAttribute('logs', '');
|
||||
$execution->setAttribute('errors', '');
|
||||
|
||||
$headers = [];
|
||||
foreach (($executionResponse['headers'] ?? []) as $key => $value) {
|
||||
$headers[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
|
||||
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
|
||||
$execution->setAttribute('responseHeaders', $headers);
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
|
||||
$encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name'));
|
||||
@@ -179,6 +341,15 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques
|
||||
}
|
||||
}
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($execution['responseHeaders'] as $header) {
|
||||
if (\strtolower($header['name']) === 'content-type') {
|
||||
$contentType = $header['value'];
|
||||
}
|
||||
|
||||
$response->setHeader($header['name'], $header['value']);
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType($contentType)
|
||||
->setStatusCode($execution['responseStatusCode'] ?? 200)
|
||||
@@ -205,13 +376,17 @@ App::init()
|
||||
->inject('console')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('getProjectDB')
|
||||
->inject('user')
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('clients')
|
||||
->inject('servers')
|
||||
->inject('queueForCertificates')
|
||||
->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, Certificate $queueForCertificates) {
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForUsage')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -220,7 +395,7 @@ App::init()
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -573,7 +748,11 @@ App::options()
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) {
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForUsage')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -581,7 +760,7 @@ App::options()
|
||||
$mainDomain = App::getEnv('_APP_DOMAIN', '');
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -605,66 +784,58 @@ App::error()
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('loggerBreadcrumbs')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$publish = true;
|
||||
|
||||
if ($error instanceof AppwriteException) {
|
||||
$publish = $error->isPublishable();
|
||||
} else {
|
||||
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
|
||||
}
|
||||
|
||||
if ($logger && $publish) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$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));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$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());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
if ($logger && ($publish || $error->getCode() === 0)) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$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));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$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());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$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('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
$code = $error->getCode();
|
||||
@@ -767,10 +938,10 @@ App::error()
|
||||
->setParam('development', App::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('type', $type)
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace);
|
||||
->setParam('message', $output['message'] ?? '')
|
||||
->setParam('type', $output['type'] ?? '')
|
||||
->setParam('code', $output['code'] ?? '')
|
||||
->setParam('trace', $output['trace'] ?? []);
|
||||
|
||||
$response->html($layout->render());
|
||||
}
|
||||
|
||||
+131
-72
@@ -8,8 +8,8 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Utopia\App;
|
||||
@@ -48,43 +48,95 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
return $label;
|
||||
};
|
||||
|
||||
$databaseListener = function (string $event, Document $document, Stats $usage) {
|
||||
$multiplier = 1;
|
||||
$databaseListener = function (string $event, Document $document, Document $project, Usage $queueForUsage, Database $dbForProject) {
|
||||
|
||||
$value = 1;
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$multiplier = -1;
|
||||
$value = -1;
|
||||
}
|
||||
|
||||
$collection = $document->getCollection();
|
||||
switch ($collection) {
|
||||
case 'users':
|
||||
$usage->setParam('users.{scope}.count.total', 1 * $multiplier);
|
||||
switch (true) {
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
case 'databases':
|
||||
$usage->setParam('databases.{scope}.count.total', 1 * $multiplier);
|
||||
case $document->getCollection() === 'users':
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_USERS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case 'buckets':
|
||||
$usage->setParam('buckets.{scope}.count.total', 1 * $multiplier);
|
||||
case $document->getCollection() === 'sessions': // sessions
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
break;
|
||||
case 'deployments':
|
||||
$usage->setParam('deployments.{scope}.storage.size', $document->getAttribute('size') * $multiplier);
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_DATABASES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value) // per database
|
||||
;
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_DOCUMENTS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': //buckets
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'bucket_'): // files
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$bucketInternalId = $parts[1];
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_FILES, $value) // per project
|
||||
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
|
||||
break;
|
||||
case $document->getCollection() === 'functions':
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'deployments':
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value)// per function
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
|
||||
break;
|
||||
default:
|
||||
if (strpos($collection, 'bucket_') === 0) {
|
||||
$usage
|
||||
->setParam('bucketId', $document->getAttribute('bucketId'))
|
||||
->setParam('files.{scope}.storage.size', $document->getAttribute('sizeOriginal') * $multiplier)
|
||||
->setParam('files.{scope}.count.total', 1 * $multiplier);
|
||||
} elseif (strpos($collection, 'database_') === 0) {
|
||||
$usage
|
||||
->setParam('databaseId', $document->getAttribute('databaseId'));
|
||||
if (strpos($collection, '_collection_') !== false) {
|
||||
$usage
|
||||
->setParam('collectionId', $document->getAttribute('$collectionId'))
|
||||
->setParam('documents.{scope}.count.total', 1 * $multiplier);
|
||||
} else {
|
||||
$usage->setParam('collections.{scope}.count.total', 1 * $multiplier);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -103,8 +155,8 @@ App::init()
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->inject('queueForMails')
|
||||
->inject('usage')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Database $dbForProject, string $mode, Mail $queueForMails, Stats $usage) use ($databaseListener) {
|
||||
->inject('queueForUsage')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Database $dbForProject, string $mode, Mail $queueForMails, Usage $queueForUsage) use ($databaseListener) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
@@ -125,6 +177,7 @@ App::init()
|
||||
$end = $request->getContentRangeEnd();
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{projectId}', $project->getId())
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
@@ -189,19 +242,14 @@ App::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
$usage
|
||||
->setParam('projectInternalId', $project->getInternalId())
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('project.{scope}.network.requests', 1)
|
||||
->setParam('httpMethod', $request->getMethod())
|
||||
->setParam('project.{scope}.network.inbound', 0)
|
||||
->setParam('project.{scope}.network.outbound', 0);
|
||||
|
||||
$queueForDeletes->setProject($project);
|
||||
$queueForDatabase->setProject($project);
|
||||
|
||||
$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));
|
||||
$dbForProject
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject))
|
||||
;
|
||||
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
|
||||
@@ -288,7 +336,7 @@ App::init()
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
if (($auths['usersAuthMagicURL'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
@@ -299,6 +347,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone 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');
|
||||
@@ -312,7 +366,7 @@ App::init()
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication type: ' . $route->getLabel('auth.type', ''));
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -365,14 +419,14 @@ App::shutdown()
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForAudits')
|
||||
->inject('usage')
|
||||
->inject('queueForUsage')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForFunctions')
|
||||
->inject('mode')
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Stats $usage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -525,36 +579,41 @@ App::shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
|
||||
&& $project->getId()
|
||||
&& !empty($route->getLabel('sdk.namespace', null))
|
||||
) { // Don't calculate console usage on admin mode
|
||||
$metric = $route->getLabel('usage.metric', '');
|
||||
$usageParams = $route->getLabel('usage.params', []);
|
||||
|
||||
if (!empty($metric)) {
|
||||
$usage->setParam($metric, 1);
|
||||
foreach ($usageParams as $param) {
|
||||
$param = $parseLabel($param, $responsePayload, $requestParams, $user);
|
||||
$parts = explode(':', $param);
|
||||
if (count($parts) != 2) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Usage params not properly set');
|
||||
}
|
||||
$usage->setParam($parts[0], $parts[1]);
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if ($mode !== APP_MODE_ADMIN) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
}
|
||||
|
||||
$queueForUsage
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user last activity
|
||||
*/
|
||||
if (!$user->isEmpty()) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCCESS)) > $accessedAt) {
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if (APP_MODE_ADMIN !== $mode) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
} else {
|
||||
$dbForConsole->updateDocument('users', $user->getId(), $user);
|
||||
}
|
||||
}
|
||||
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$usage
|
||||
->setParam('project.{scope}.network.inbound', $request->getSize() + $fileSize)
|
||||
->setParam('project.{scope}.network.outbound', $response->getSize())
|
||||
->submit();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ App::init()
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
if (($auths['usersAuthMagicURL'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
@@ -43,6 +43,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone 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');
|
||||
|
||||
+1
-6
@@ -263,10 +263,9 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs");
|
||||
$route = $app->getRoute();
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
$log = $app->getResource("log");
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
@@ -298,10 +297,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
+32
-36
@@ -19,6 +19,7 @@ if (\file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
\error_reporting(E_ALL);
|
||||
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Audit;
|
||||
@@ -32,7 +33,6 @@ 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 Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Logger\Logger;
|
||||
@@ -73,14 +73,17 @@ use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Logger\Log;
|
||||
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\Hostname;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -110,7 +113,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
|
||||
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 = 329;
|
||||
const APP_CACHE_BUSTER = 331;
|
||||
const APP_VERSION_STABLE = '1.4.13';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
@@ -175,10 +178,6 @@ 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';
|
||||
const COMPRESSION_TYPE_ZSTD = 'zstd';
|
||||
// Mail Types
|
||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||
@@ -198,6 +197,7 @@ const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
|
||||
// Usage metrics
|
||||
const METRIC_TEAMS = 'teams';
|
||||
const METRIC_USERS = 'users';
|
||||
const METRIC_MESSAGES = 'messages';
|
||||
const METRIC_SESSIONS = 'sessions';
|
||||
const METRIC_DATABASES = 'databases';
|
||||
const METRIC_COLLECTIONS = 'collections';
|
||||
@@ -244,6 +244,7 @@ Config::load('platforms', __DIR__ . '/config/platforms.php');
|
||||
Config::load('collections', __DIR__ . '/config/collections.php');
|
||||
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
|
||||
Config::load('runtimes-v2', __DIR__ . '/config/runtimes-v2.php');
|
||||
Config::load('usage', __DIR__ . '/config/usage.php');
|
||||
Config::load('roles', __DIR__ . '/config/roles.php'); // User roles and scopes
|
||||
Config::load('scopes', __DIR__ . '/config/scopes.php'); // User roles and scopes
|
||||
Config::load('services', __DIR__ . '/config/services.php'); // List of services
|
||||
@@ -777,31 +778,7 @@ $register->set('db', function () {
|
||||
|
||||
return $pdo;
|
||||
});
|
||||
$register->set('influxdb', function () {
|
||||
|
||||
// Register DB connection
|
||||
$host = App::getEnv('_APP_INFLUXDB_HOST', '');
|
||||
$port = App::getEnv('_APP_INFLUXDB_PORT', '');
|
||||
|
||||
if (empty($host) || empty($port)) {
|
||||
return;
|
||||
}
|
||||
$driver = new InfluxDB\Driver\Curl("http://{$host}:{$port}");
|
||||
$client = new InfluxDB\Client($host, $port, '', '', false, false, 5);
|
||||
$client->setDriver($driver);
|
||||
|
||||
return $client;
|
||||
});
|
||||
$register->set('statsd', function () {
|
||||
// Register DB connection
|
||||
$host = App::getEnv('_APP_STATSD_HOST', 'telegraf');
|
||||
$port = App::getEnv('_APP_STATSD_PORT', 8125);
|
||||
|
||||
$connection = new \Domnikl\Statsd\Connection\UdpSocket($host, $port);
|
||||
$statsd = new \Domnikl\Statsd\Client($connection);
|
||||
|
||||
return $statsd;
|
||||
});
|
||||
$register->set('smtp', function () {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
@@ -842,6 +819,9 @@ $register->set('passwordsDictionary', function () {
|
||||
$register->set('promiseAdapter', function () {
|
||||
return new Swoole();
|
||||
});
|
||||
$register->set('hooks', function () {
|
||||
return new Hooks();
|
||||
});
|
||||
/*
|
||||
* Localization
|
||||
*/
|
||||
@@ -877,13 +857,14 @@ foreach ($locales as $locale) {
|
||||
]);
|
||||
|
||||
// Runtime Execution
|
||||
App::setResource('log', fn() => new Log());
|
||||
App::setResource('logger', function ($register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('loggerBreadcrumbs', function () {
|
||||
return [];
|
||||
});
|
||||
App::setResource('hooks', function ($register) {
|
||||
return $register->get('hooks');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('register', fn() => $register);
|
||||
App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')));
|
||||
@@ -920,15 +901,15 @@ App::setResource('queueForAudits', function (Connection $queue) {
|
||||
App::setResource('queueForFunctions', function (Connection $queue) {
|
||||
return new Func($queue);
|
||||
}, ['queue']);
|
||||
App::setResource('queueForUsage', function (Connection $queue) {
|
||||
return new Usage($queue);
|
||||
}, ['queue']);
|
||||
App::setResource('queueForCertificates', function (Connection $queue) {
|
||||
return new Certificate($queue);
|
||||
}, ['queue']);
|
||||
App::setResource('queueForMigrations', function (Connection $queue) {
|
||||
return new Migration($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'),
|
||||
@@ -937,6 +918,21 @@ App::setResource('clients', function ($request, $console, $project) {
|
||||
'hostname' => $request->getHostname(),
|
||||
], Document::SET_TYPE_APPEND);
|
||||
|
||||
$hostnames = explode(',', App::getEnv('_APP_CONSOLE_HOSTNAMES', ''));
|
||||
$validator = new Hostname();
|
||||
foreach ($hostnames as $hostname) {
|
||||
$hostname = trim($hostname);
|
||||
if (!$validator->isValid($hostname)) {
|
||||
continue;
|
||||
}
|
||||
$console->setAttribute('platforms', [
|
||||
'$collection' => ID::custom('platforms'),
|
||||
'type' => Origin::CLIENT_TYPE_WEB,
|
||||
'name' => $hostname,
|
||||
'hostname' => $hostname,
|
||||
], Document::SET_TYPE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get All verified client URLs for both console and current projects
|
||||
* + Filter for duplicated entries
|
||||
|
||||
@@ -71,7 +71,6 @@ services:
|
||||
- mariadb
|
||||
- redis
|
||||
# - clamav
|
||||
- influxdb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -79,6 +78,7 @@ services:
|
||||
- _APP_CONSOLE_WHITELIST_ROOT
|
||||
- _APP_CONSOLE_WHITELIST_EMAILS
|
||||
- _APP_CONSOLE_WHITELIST_IPS
|
||||
- _APP_CONSOLE_HOSTNAMES
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
@@ -106,8 +106,6 @@ services:
|
||||
- _APP_SMTP_USERNAME
|
||||
- _APP_SMTP_PASSWORD
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
- _APP_STORAGE_LIMIT
|
||||
- _APP_STORAGE_PREVIEW_LIMIT
|
||||
- _APP_STORAGE_ANTIVIRUS
|
||||
@@ -144,9 +142,8 @@ services:
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_STATSD_HOST
|
||||
- _APP_STATSD_PORT
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
@@ -273,7 +270,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
@@ -420,7 +417,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
environment:
|
||||
@@ -595,16 +592,15 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
|
||||
appwrite-usage:
|
||||
appwrite-worker-usage:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: usage
|
||||
container_name: appwrite-usage
|
||||
container_name: appwrite-worker-usage
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
depends_on:
|
||||
- influxdb
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
@@ -615,9 +611,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -652,7 +645,7 @@ services:
|
||||
- _APP_DB_PASS
|
||||
|
||||
appwrite-assistant:
|
||||
image: appwrite/assistant:0.2.2
|
||||
image: appwrite/assistant:0.3.0
|
||||
container_name: appwrite-assistant
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
@@ -751,27 +744,6 @@ services:
|
||||
# volumes:
|
||||
# - appwrite-uploads:/storage/uploads
|
||||
|
||||
influxdb:
|
||||
image: appwrite/influxdb:1.5.0
|
||||
container_name: appwrite-influxdb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-influxdb:/var/lib/influxdb:rw
|
||||
|
||||
telegraf:
|
||||
image: appwrite/telegraf:1.4.0
|
||||
container_name: appwrite-telegraf
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
|
||||
networks:
|
||||
gateway:
|
||||
name: gateway
|
||||
@@ -788,5 +760,4 @@ volumes:
|
||||
appwrite-certificates:
|
||||
appwrite-functions:
|
||||
appwrite-builds:
|
||||
appwrite-influxdb:
|
||||
appwrite-config:
|
||||
|
||||
+40
-12
@@ -11,11 +11,11 @@ use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Hamster;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Phone;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Event\UsageDump;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Usage\Stats;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
@@ -23,6 +23,7 @@ use Utopia\Cache\Cache;
|
||||
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\Platform\Service;
|
||||
@@ -89,6 +90,17 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
|
||||
return $database;
|
||||
}, ['cache', 'register', 'message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForConsole) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
return $dbForConsole->getDocument('projects', $project->getId());
|
||||
;
|
||||
}, ['message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
@@ -142,6 +154,18 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
|
||||
};
|
||||
}, ['pools', 'dbForConsole', 'cache']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
|
||||
});
|
||||
|
||||
Server::setResource('auditRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600));
|
||||
});
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600));
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
$pools = $register->get('pools');
|
||||
$list = Config::getParam('pools-cache', []);
|
||||
@@ -158,9 +182,12 @@ Server::setResource('cache', function (Registry $register) {
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['register']);
|
||||
Server::setResource('log', fn() => new Log());
|
||||
Server::setResource('usage', function ($register) {
|
||||
return new Stats($register->get('statsd'));
|
||||
}, ['register']);
|
||||
Server::setResource('queueForUsage', function (Connection $queue) {
|
||||
return new Usage($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('queueForUsageDump', function (Connection $queue) {
|
||||
return new UsageDump($queue);
|
||||
}, ['queue']);
|
||||
Server::setResource('queue', function (Group $pools) {
|
||||
return $pools->get('queue')->pop()->getResource();
|
||||
}, ['pools']);
|
||||
@@ -264,7 +291,6 @@ try {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
}
|
||||
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
$worker
|
||||
@@ -279,7 +305,8 @@ $worker
|
||||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log) {
|
||||
->inject('project')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($error instanceof PDOException) {
|
||||
@@ -295,6 +322,7 @@ $worker
|
||||
$log->setAction('appwrite-queue-' . App::getEnv('QUEUE'));
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId() ?? 'n/a');
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
@@ -314,9 +342,9 @@ $worker
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
});
|
||||
|
||||
$worker->workerStart()
|
||||
->action(function () use ($workerName) {
|
||||
Console::info("Worker $workerName started");
|
||||
});
|
||||
$worker->workerStart()
|
||||
->action(function () use ($workerName) {
|
||||
Console::info("Worker $workerName started");
|
||||
});
|
||||
|
||||
$worker->start();
|
||||
$worker->start();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php create-inf-metric $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=failed $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=processing $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=success $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-retry $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/worker.php usage $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/worker.php usage-dump $@
|
||||
+9
-10
@@ -1,4 +1,5 @@
|
||||
{
|
||||
|
||||
"name": "appwrite/server-ce",
|
||||
"description": "End to end backend server for frontend and mobile apps.",
|
||||
"type": "project",
|
||||
@@ -45,24 +46,24 @@
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "0.34.*",
|
||||
"utopia-php/analytics": "0.10.*",
|
||||
"utopia-php/audit": "0.36.*",
|
||||
"utopia-php/cache": "0.8.*",
|
||||
"utopia-php/audit": "0.35.*",
|
||||
"utopia-php/cache": "0.9.*",
|
||||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.46.*",
|
||||
"utopia-php/domains": "0.3.*",
|
||||
"utopia-php/database": "0.45.*",
|
||||
"utopia-php/domains": "0.5.*",
|
||||
"utopia-php/dsn": "0.1.*",
|
||||
"utopia-php/framework": "0.31.0",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/framework": "0.33.*",
|
||||
"utopia-php/image": "0.6.*",
|
||||
"utopia-php/locale": "0.4.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/messaging": "0.2.*",
|
||||
"utopia-php/messaging": "0.3.*",
|
||||
"utopia-php/migration": "0.3.*",
|
||||
"utopia-php/orchestration": "0.9.*",
|
||||
"utopia-php/platform": "0.5.*",
|
||||
"utopia-php/pools": "0.4.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.5.*",
|
||||
"utopia-php/queue": "0.7.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.18.*",
|
||||
"utopia-php/swoole": "0.5.*",
|
||||
@@ -70,12 +71,10 @@
|
||||
"utopia-php/websocket": "0.1.*",
|
||||
"matomo/device-detector": "6.1.*",
|
||||
"dragonmantank/cron-expression": "3.3.2",
|
||||
"influxdb/influxdb-php": "1.15.2",
|
||||
"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",
|
||||
"league/csv": "9.7.1"
|
||||
},
|
||||
"repositories": [
|
||||
|
||||
Generated
+226
-928
File diff suppressed because it is too large
Load Diff
+66
-88
@@ -5,12 +5,12 @@
|
||||
|
||||
x-logging: &x-logging
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-file: '5'
|
||||
max-size: '10m'
|
||||
max-file: "5"
|
||||
max-size: "10m"
|
||||
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
DEBUG: false
|
||||
TESTING: true
|
||||
VERSION: dev
|
||||
ports:
|
||||
ports:
|
||||
- 9501:80
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -88,7 +88,7 @@ services:
|
||||
- mariadb
|
||||
- redis
|
||||
# - clamav
|
||||
entrypoint:
|
||||
entrypoint:
|
||||
- php
|
||||
- -e
|
||||
- app/http.php
|
||||
@@ -100,6 +100,7 @@ services:
|
||||
- _APP_CONSOLE_WHITELIST_ROOT
|
||||
- _APP_CONSOLE_WHITELIST_EMAILS
|
||||
- _APP_CONSOLE_WHITELIST_IPS
|
||||
- _APP_CONSOLE_HOSTNAMES
|
||||
- _APP_SYSTEM_EMAIL_NAME
|
||||
- _APP_SYSTEM_EMAIL_ADDRESS
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
@@ -127,8 +128,6 @@ services:
|
||||
- _APP_SMTP_USERNAME
|
||||
- _APP_SMTP_PASSWORD
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
- _APP_STORAGE_LIMIT
|
||||
- _APP_STORAGE_PREVIEW_LIMIT
|
||||
- _APP_STORAGE_ANTIVIRUS
|
||||
@@ -165,8 +164,6 @@ services:
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_STATSD_HOST
|
||||
- _APP_STATSD_PORT
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -195,7 +192,7 @@ services:
|
||||
container_name: appwrite-realtime
|
||||
image: appwrite-dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
ports:
|
||||
- 9505:80
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
@@ -303,7 +300,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
@@ -357,7 +354,7 @@ services:
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
@@ -456,7 +453,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
volumes:
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- ./app:/usr/src/code/app
|
||||
@@ -574,6 +571,7 @@ services:
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_SMS_PROVIDER
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROJECTS_DENY_LIST
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -647,20 +645,20 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
|
||||
appwrite-usage:
|
||||
entrypoint: usage
|
||||
appwrite-worker-usage:
|
||||
entrypoint: worker-usage
|
||||
<<: *x-logging
|
||||
container_name: appwrite-usage
|
||||
container_name: appwrite-worker-usage
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/local/dev
|
||||
depends_on:
|
||||
- influxdb
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
@@ -671,9 +669,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_INFLUXDB_HOST
|
||||
- _APP_INFLUXDB_PORT
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -681,6 +676,38 @@ services:
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
|
||||
appwrite-worker-usage-dump:
|
||||
entrypoint: worker-usage-dump
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-usage-dump
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL
|
||||
|
||||
appwrite-schedule:
|
||||
entrypoint: schedule
|
||||
@@ -711,7 +738,7 @@ services:
|
||||
|
||||
appwrite-assistant:
|
||||
container_name: appwrite-assistant
|
||||
image: appwrite/assistant:0.2.2
|
||||
image: appwrite/assistant:0.3.0
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
@@ -744,7 +771,7 @@ services:
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_MIXPANEL_TOKEN
|
||||
|
||||
|
||||
appwrite-hamster-scheduler:
|
||||
entrypoint: hamster
|
||||
<<: *x-logging
|
||||
@@ -862,7 +889,7 @@ services:
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
command: 'mysqld --innodb-flush-method=fsync' # add ' --query_cache_size=0' for DB tests
|
||||
command: "mysqld --innodb-flush-method=fsync" # add ' --query_cache_size=0' for DB tests
|
||||
# command: mv /var/lib/mysql/ib_logfile0 /var/lib/mysql/ib_logfile0.bu && mv /var/lib/mysql/ib_logfile1 /var/lib/mysql/ib_logfile1.bu
|
||||
|
||||
# smtp:
|
||||
@@ -900,26 +927,6 @@ services:
|
||||
# - appwrite
|
||||
# volumes:
|
||||
# - appwrite-uploads:/storage/uploads
|
||||
|
||||
influxdb:
|
||||
image: appwrite/influxdb:1.5.0
|
||||
container_name: appwrite-influxdb
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-influxdb:/var/lib/influxdb:rw
|
||||
|
||||
telegraf:
|
||||
image: appwrite/telegraf:1.4.0
|
||||
container_name: appwrite-telegraf
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
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
|
||||
@@ -928,16 +935,16 @@ services:
|
||||
#
|
||||
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
|
||||
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
|
||||
# RedisCommander - A nice UI for exploring Redis data
|
||||
# Chronograf - A nice UI for exploring InfluxDB data
|
||||
# Webgrind - A nice UI for exploring and debugging code-level stuff
|
||||
# Redis Insight - A nice UI for exploring Redis data
|
||||
# Adminer - A nice UI for exploring MariaDB data
|
||||
# GraphQl Explorer - A nice UI for exploring GraphQL API
|
||||
|
||||
maildev: # used mainly for dev tests
|
||||
image: appwrite/mailcatcher:1.0.0
|
||||
container_name: appwrite-mailcatcher
|
||||
<<: *x-logging
|
||||
ports:
|
||||
- '9503:1080'
|
||||
- "9503:1080"
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -946,7 +953,7 @@ services:
|
||||
container_name: appwrite-requestcatcher
|
||||
<<: *x-logging
|
||||
ports:
|
||||
- '9504:5000'
|
||||
- "9504:5000"
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -960,42 +967,16 @@ services:
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
# redis-commander:
|
||||
# image: rediscommander/redis-commander:latest
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - appwrite
|
||||
# environment:
|
||||
# - REDIS_HOSTS=redis
|
||||
# ports:
|
||||
# - "8081:8081"
|
||||
redis-insight:
|
||||
image: redis/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- REDIS_HOSTS=redis
|
||||
ports:
|
||||
- "8081:5540"
|
||||
|
||||
# chronograf:
|
||||
# image: chronograf:1.6
|
||||
# container_name: appwrite-chronograf
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - appwrite
|
||||
# volumes:
|
||||
# - appwrite-chronograf:/var/lib/chronograf
|
||||
# ports:
|
||||
# - "8888:8888"
|
||||
# environment:
|
||||
# - INFLUXDB_URL=http://influxdb:8086
|
||||
# - KAPACITOR_URL=http://kapacitor:9092
|
||||
# - AUTH_DURATION=48h
|
||||
# - TOKEN_SECRET=duperduper5674829!jwt
|
||||
# - GH_CLIENT_ID=d86f7145a41eacfc52cc
|
||||
# - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab
|
||||
# - GH_ORGS=appwrite
|
||||
|
||||
# webgrind:
|
||||
# image: 'jokkedk/webgrind:latest'
|
||||
# volumes:
|
||||
# - './debug:/tmp'
|
||||
# ports:
|
||||
# - '3001:80'
|
||||
|
||||
graphql-explorer:
|
||||
container_name: appwrite-graphql-explorer
|
||||
image: appwrite/altair:0.3.0
|
||||
@@ -1007,7 +988,6 @@ services:
|
||||
environment:
|
||||
- SERVER_URL=http://localhost/v1/graphql
|
||||
|
||||
|
||||
# Dev Tools End ------------------------------------------------------------------------------------------
|
||||
|
||||
networks:
|
||||
@@ -1026,6 +1006,4 @@ volumes:
|
||||
appwrite-certificates:
|
||||
appwrite-functions:
|
||||
appwrite-builds:
|
||||
appwrite-influxdb:
|
||||
appwrite-config:
|
||||
# appwrite-chronograf:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_phone_verification()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_recovery(email: 'email@example.com', url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.create_verification(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_identity(identity_id: '[IDENTITY_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.delete_sessions()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get_prefs()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.get()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_identities()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_logs()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.list_sessions()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_email(email: 'email@example.com', password: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_name(name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_password(password: '')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_phone_verification(user_id: '[USER_ID]', secret: '[SECRET]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_phone(phone: '+12065550100', password: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_prefs(prefs: {})
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_recovery(user_id: '[USER_ID]', secret: '[SECRET]', password: 'password', password_again: 'password')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_session(session_id: '[SESSION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_status()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ account = Account.new(client)
|
||||
|
||||
response = account.update_verification(user_id: '[USER_ID]', secret: '[SECRET]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_browser(code: 'aa')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_credit_card(code: 'amex')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_favicon(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_flag(code: 'af')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_image(url: 'https://example.com')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_initials()
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ avatars = Avatars.new(client)
|
||||
|
||||
response = avatars.get_qr(text: '[TEXT]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_boolean_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_collection(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_datetime_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_document(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', document_id: '[DOCUMENT_ID]', data: {})
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_email_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_enum_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', elements: [], required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_float_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_index(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', type: 'key', attributes: [])
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_integer_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_ip_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_relationship_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', related_collection_id: '[RELATED_COLLECTION_ID]', type: 'oneToOne')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_string_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', size: 1, required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create_url_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '', required: false)
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.create(database_id: '[DATABASE_ID]', name: '[NAME]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_attribute(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]', key: '')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require 'Appwrite'
|
||||
require 'appwrite'
|
||||
|
||||
include Appwrite
|
||||
|
||||
@@ -11,4 +11,4 @@ databases = Databases.new(client)
|
||||
|
||||
response = databases.delete_collection(database_id: '[DATABASE_ID]', collection_id: '[COLLECTION_ID]')
|
||||
|
||||
puts response.inspect
|
||||
puts response.inspect
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user