Merge remote-tracking branch 'origin/master' into feat-graphql-support

# Conflicts:
#	app/controllers/api/databases.php
#	app/controllers/general.php
#	app/init.php
#	composer.json
#	composer.lock
#	docker-compose.yml
#	phpunit.xml
This commit is contained in:
Jake Barnby
2022-07-01 00:05:44 +12:00
2112 changed files with 47061 additions and 4973 deletions
+2
View File
@@ -56,6 +56,8 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_PHONE_PROVIDER=phone://mock
_APP_PHONE_FROM=+123456789
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000
+1
View File
@@ -37,6 +37,7 @@ body:
label: "🎲 Appwrite version"
description: "What version of Appwrite are you running?"
options:
- Version 0.15.x
- Version 0.14.x
- Version 0.13.x
- Version 0.12.x
+3 -3
View File
@@ -41,6 +41,6 @@ jobs:
- name: Teardown
if: always()
run: |
docker ps -aq | xargs docker rm --force
docker volume prune --force
docker network prune --force
docker ps -aq | xargs docker rm --force || true
docker volume prune --force || true
docker network prune --force || true
+93 -1
View File
@@ -1,4 +1,96 @@
- Added support for selfhosted Gitlab (Oauth)
# Version 0.15.1
## Bugs
- Fixed SMS for `createVerification` by @christyjacob4 in https://github.com/appwrite/appwrite/pull/3454
- Fixed missing Attributes when creating an Index by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3461
- Fixed broken Link for Documents under Collections by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3461
- Fixed all `$createdAt` and `$updatedAt` occurences in the UI by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3461
- Fixed Delete Document from the UI by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3463
- Fixed internal Attribute and Index key on Migration by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3455
## Docs
- Updated Phone Authentication by @christyjacob4 in https://github.com/appwrite/appwrite/pull/3456
# Version 0.15.0
## BREAKING CHANGES
- Docker Compose V2 is required now
- The `POST:/v1/account/sessions` endpoint is now `POST:/v1/account/sessions/email`
- All `/v1/database/...` endpoints are now `/v1/databases/...`
- `dateCreated` attribute is removed from Teams
- `dateCreated` attribute is removed from Executions
- `dateCreated` attribute is removed from Files
- `dateCreated` and `dateUpdated` attributes are removed from Functions
- `dateCreated` and `dateUpdated` attributes are removed from Deployments
- `dateCreated` and `dateUpdated` attributes are removed from Buckets
- Following Events for Webhooks and Functions are changed:
- `collections.[COLLECTION_ID]` is now `databases.[DATABASE_ID].collections.[COLLECTION_ID]`
- `collections.[COLLECTION_ID].documents.[DOCUMENT_ID]` is now `databases.[DATABASE_ID].collections.[COLLECTION_ID].documents.[DOCUMENT_ID]`
- Following Realtime Channels are changed:
- `collections.[COLLECTION_ID]` is now `databases.[DATABASE_ID].ollections.[COLLECTION_ID]`
- `collections.[COLLECTION_ID].documents` is now `databases.[DATABASE_ID].ollections.[COLLECTION_ID].documents`
- After Migration a Database called `default` is created for all your existing Database Collections
## Features
- Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357
- Added Twilio Support
- Added TextMagic Support
- Added Telesign Support
- Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`)
- Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`)
- Added Endpoint to update Account Phone Number (`PATCH:/v1/account/phone`)
- Added Endpoint to create Account Phone Verification (`POST:/v1/account/verification/phone`)
- Added Endpoint to confirm Account Phone Verification (`PUT:/v1/account/verification/phone`)
- Added `_APP_PHONE_PROVIDER` and `_APP_PHONE_FROM` Environment Variable
- Added `phone` and `phoneVerification` Attribute to User
- Added `$createdAt` and `$updatedAt` Attributes by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3382
- Bucket
- Collection
- Deployment
- Document
- Domain
- Execution
- File
- Func
- Key
- Membership
- Platform
- Project
- Team
- User
- Webhook
- Session (only `$createdAt`)
- Token (only `$createdAt`)
- Added Databases Resource to the Database Service by @lohanidamodar in https://github.com/appwrite/appwrite/pull/3338
- Added `databases.read` and `databases.write` Scopes for API Keys
- Added New Runtimes
- Dart 2.17
- Deno 1.21
- Java 18
- Node 18
- Webhooks now have a Signature Key for proof of Origin by @shimonewman in https://github.com/appwrite/appwrite/pull/3351
- Start using Docker Compose V2 (from `docker-compose` to `docker compose`) by @Meldiron in https://github.com/appwrite/appwrite/pull/3362
- Added support for selfhosted Gitlab (OAuth) by @Meldiron in https://github.com/appwrite/appwrite/pull/3366
- Added Dailymotion OAuth Provider by @2002Bishwajeet in https://github.com/appwrite/appwrite/pull/3371
- Added Autodesk OAuth Provider by @Haimantika in https://github.com/appwrite/appwrite/pull/3420
- Ignore Service Checks when using API Key by @stnguyen90 in https://github.com/appwrite/appwrite/pull/3270
- Added WebM as MIME- and Preview Type by @chuongtang in https://github.com/appwrite/appwrite/pull/3327
- Expired User Sessions are now deleted by the Maintenance Worker by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3324
- Increased JWT rate-limit to 100 per hour by @abnegate in https://github.com/appwrite/appwrite/pull/3345
- Internal Database Relations are now resolved using the Internal ID by @fogelito in https://github.com/appwrite/appwrite/pull/3383
- Permissions for Documents can be updated without payload now by @gepd in https://github.com/appwrite/appwrite/pull/3346
## Bugs
- Fixed Zoom OAuth scopes
- Fixed empty build logs for Functions
- Fixed unnecessary SMTP check on Team Invite using an API Key by @stnguyen90 in https://github.com/appwrite/appwrite/pull/3270
- Fixed Error Message when adding Team Member to project by @stnguyen90 in https://github.com/appwrite/appwrite/pull/3296
- Fixed .NET Runtime Logo by @adityaoberai in https://github.com/appwrite/appwrite/pull/3315
- Fixed unnecessary Function execution delays by @Meldiron in https://github.com/appwrite/appwrite/pull/3348
- Fixed Runtime race conditions on cold start by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/3361
- Fixed Malayalam translation by @varghesejose2020 in https://github.com/appwrite/appwrite/pull/2561
- Fixed English translation by @MATsxm in https://github.com/appwrite/appwrite/pull/3337
- Fixed spelling in Realtime Worker logs by @gireeshp in https://github.com/appwrite/appwrite/pull/1663
- Fixed Docs URL for Yammer OAuth by @everly-gif in https://github.com/appwrite/appwrite/pull/3402
# Version 0.14.2
+8 -8
View File
@@ -93,7 +93,7 @@ git clone git@github.com:[YOUR_FORK_HERE]/appwrite.git
cd appwrite
docker-compose up -d
docker compose up -d
```
### Code Autocompletion
@@ -108,7 +108,7 @@ docker run --rm --interactive --tty \
### User Interface
Appwrite uses an internal micro-framework called Litespeed.js to build simple UI components in vanilla JS and [less](http://lesscss.org/) for compiling CSS code. To apply any of your changes to the UI, use the `gulp build` or `gulp less` commands, and restart the Appwrite main container to load the new static files to memory using `docker-compose restart appwrite`.
Appwrite uses an internal micro-framework called Litespeed.js to build simple UI components in vanilla JS and [less](http://lesscss.org/) for compiling CSS code. To apply any of your changes to the UI, use the `gulp build` or `gulp less` commands, and restart the Appwrite main container to load the new static files to memory using `docker compose restart appwrite`.
### Get Started
@@ -222,7 +222,7 @@ Currently, all of the Appwrite microservices are intended to communicate using t
## Ports
Appwrite dev version uses ports 80 and 443 as an entry point to the Appwrite API and console. We also expose multiple ports in the range of 9500-9504 for debugging some of the Appwrite containers on dev mode. If you have any conflicts with the ports running on your system, you can easily replace them by editing Appwrite's docker-compose.yml file and executing `docker-compose up -d` command.
Appwrite dev version uses ports 80 and 443 as an entry point to the Appwrite API and console. We also expose multiple ports in the range of 9500-9504 for debugging some of the Appwrite containers on dev mode. If you have any conflicts with the ports running on your system, you can easily replace them by editing Appwrite's docker-compose.yml file and executing `docker compose up -d` command.
## Technology Stack
@@ -363,25 +363,25 @@ In settings, go to **Languages & Frameworks** > **PHP** > **Debug**, there under
To run all tests manually, use the Appwrite Docker CLI from your terminal:
```bash
docker-compose exec appwrite test
docker compose exec appwrite test
```
To run unit tests use:
```bash
docker-compose exec appwrite test /usr/src/code/tests/unit
docker compose exec appwrite test /usr/src/code/tests/unit
```
To run end-2-end tests use:
```bash
docker-compose exec appwrite test /usr/src/code/tests/e2e
docker compose exec appwrite test /usr/src/code/tests/e2e
```
To run end-2-end tests for a specific service use:
```bash
docker-compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName]
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName]
```
## Benchmarking
@@ -462,4 +462,4 @@ Submitting documentation updates, enhancements, designs, or bug fixes. Spelling
### Helping Someone
Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo!
Searching for Appwrite on Discord, GitHub, or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Appwrite's repo!
+14 -3
View File
@@ -31,7 +31,7 @@ ENV DEBUG=$DEBUG
ENV PHP_REDIS_VERSION=5.3.7 \
PHP_MONGODB_VERSION=1.13.0 \
PHP_SWOOLE_VERSION=v4.8.9 \
PHP_SWOOLE_VERSION=v4.8.10 \
PHP_IMAGICK_VERSION=3.7.0 \
PHP_YAML_VERSION=2.2.2 \
PHP_MAXMINDDB_VERSION=v1.11.0
@@ -131,6 +131,9 @@ ARG VERSION=dev
ARG DEBUG=false
ENV DEBUG=$DEBUG
ENV DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
ENV DOCKER_COMPOSE_VERSION=v2.5.0
ENV _APP_SERVER=swoole \
_APP_ENV=production \
_APP_LOCALE=en \
@@ -190,6 +193,8 @@ ENV _APP_SERVER=swoole \
_APP_SMTP_SECURE= \
_APP_SMTP_USERNAME= \
_APP_SMTP_PASSWORD= \
_APP_PHONE_PROVIDER= \
_APP_PHONE_FROM= \
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
_APP_FUNCTIONS_TIMEOUT=900 \
_APP_FUNCTIONS_CONTAINERS=10 \
@@ -232,12 +237,17 @@ RUN \
libmaxminddb-dev \
certbot \
docker-cli \
docker-compose \
libgomp \
&& docker-php-ext-install sockets opcache pdo_mysql \
&& apk del .deps \
&& rm -rf /var/cache/apk/*
RUN \
mkdir -p $DOCKER_CONFIG/cli-plugins \
&& ARCH=$(uname -m) && if [ $ARCH == "armv7l" ]; then ARCH="armv7"; fi \
&& curl -SL https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-linux-$ARCH -o $DOCKER_CONFIG/cli-plugins/docker-compose \
&& chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
RUN \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
@@ -292,11 +302,12 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/vars && \
chmod +x /usr/local/bin/worker-audits && \
chmod +x /usr/local/bin/worker-certificates && \
chmod +x /usr/local/bin/worker-database && \
chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \
chmod +x /usr/local/bin/worker-functions && \
chmod +x /usr/local/bin/worker-builds && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-webhooks
# Letsencrypt Permissions
+5 -5
View File
@@ -59,7 +59,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
### Windows
@@ -71,7 +71,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
#### PowerShell
@@ -81,13 +81,13 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
需要自定义容器构架,请查看我们的 Docker [环境变量](https://appwrite.io/docs/environment-variables) 文档。您还可以参考我们的 [docker-compose.yml](https://gist.github.com/eldadfux/977869ff6bdd7312adfd4e629ee15cc5#file-docker-compose-yml) 文件手动设置环境。
需要自定义容器构架,请查看我们的 Docker [环境变量](https://appwrite.io/docs/environment-variables) 文档。您还可以参考我们的 [docker-compose.yml](https://appwrite.io/install/compose) 和 [.env](https://appwrite.io/install/env) 文件手动设置环境。
### 从旧版本升级
@@ -109,7 +109,7 @@ docker run -it --rm ,
* [**帐户**](https://appwrite.io/docs/client/account) -管理当前用户的帐户和登录方式。跟踪和管理用户 Session,登录设备,登录方法和查看相关记录。
* [**用户**](https://appwrite.io/docs/server/users) - 在以管理员模式登录时管理和列出所有用户。
* [**团队**](https://appwrite.io/docs/client/teams) - 管理用户分组。邀请成员,管理团队中的用户权限和用户角色。
* [**数据库**](https://appwrite.io/docs/client/database) - 管理数据库文档和文档集。用检索界面来对文档和文档集进行读取,创建,更新,和删除。
* [**数据库**](https://appwrite.io/docs/client/databases) - 管理数据库文档和文档集。用检索界面来对文档和文档集进行读取,创建,更新,和删除。
* [**贮存**](https://appwrite.io/docs/client/storage) - 管理文件的阅读、创建、删除和预览。设置文件的预览来满足程序的个性化需求。所有文件都由 ClamAV 扫描并安全存储和加密。
* [**云函数**](https://appwrite.io/docs/server/functions) - 在安全,隔离的环境中运行自定义代码。这些代码可以被事件,CRON,或者手动操作触发。
* [**语言适配**](https://appwrite.io/docs/client/locale) - 根据用户所在的的国家和地区做出合适的语言适配。
+9 -6
View File
@@ -1,3 +1,6 @@
> It's going to get cloudy! 🌩 ☂️
> The Appwrite Cloud is coming soon! You can learn more about our upcoming hosted solution and signup for free credits at: https://appwrite.io/cloud
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img width="260" height="39" src="https://appwrite.io/images/appwrite.svg" alt="Appwrite Logo"></a>
@@ -19,7 +22,7 @@
English | [简体中文](README-CN.md)
[**Appwrite 0.14 has been released! Learn what's new!**](https://dev.to/appwrite/announcing-appwrite-014-with-11-cloud-function-runtimes-36f5)
[**Appwrite 0.15 has been released! Learn what's new!**](https://dev.to/appwrite/announcing-appwrite-015-with-phone-authentication-more-5cjj)
Appwrite is an end-to-end backend server for Web, Mobile, Native, or Backend apps packaged as a set of Docker<nobr> microservices. Appwrite abstracts the complexity and repetitiveness required to build a modern backend API from scratch and allows you to build secure apps faster.
@@ -62,7 +65,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
### Windows
@@ -74,7 +77,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
#### PowerShell
@@ -84,13 +87,13 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.14.2
appwrite/appwrite:0.15.1
```
Once the Docker installation completes, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after installation completes.
For advanced production and custom installation, check out our Docker [environment variables](https://appwrite.io/docs/environment-variables) docs. You can also use our public [docker-compose.yml](https://gist.github.com/eldadfux/977869ff6bdd7312adfd4e629ee15cc5#file-docker-compose-yml) file to manually set up an environment.
For advanced production and custom installation, check out our Docker [environment variables](https://appwrite.io/docs/environment-variables) docs. You can also use our public [docker-compose.yml](https://appwrite.io/install/compose) and [.env](https://appwrite.io/install/env) files to manually set up an environment.
### Upgrade from an Older Version
@@ -112,7 +115,7 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
* [**Account**](https://appwrite.io/docs/client/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs.
* [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when in admin mode.
* [**Teams**](https://appwrite.io/docs/client/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team.
* [**Database**](https://appwrite.io/docs/client/database) - Manage database collections and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
* [**Databases**](https://appwrite.io/docs/client/databases) - Manage databases, collections and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
* [**Storage**](https://appwrite.io/docs/client/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to fit your app perfectly. All files are scanned by ClamAV and stored in a secure and encrypted way.
* [**Functions**](https://appwrite.io/docs/server/functions) - Customize your Appwrite server by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event, manually or using a CRON schedule.
* [**Locale**](https://appwrite.io/docs/client/locale) - Track your user's location, and manage your app locale-based data.
+3 -3
View File
@@ -7,7 +7,7 @@ return [
'name' => 'Email/Password',
'key' => 'emailPassword',
'icon' => '/images/users/email.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateEmailSession',
'enabled' => true,
],
'magic-url' => [
@@ -42,7 +42,7 @@ return [
'name' => 'Phone',
'key' => 'phone',
'icon' => '/images/users/phone.png',
'docs' => '',
'enabled' => false,
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreatePhoneSession',
'enabled' => true,
],
];
+276 -147
View File
@@ -16,10 +16,10 @@ $auth = Config::getParam('auth', []);
*/
$collections = [
'collections' => [
'databases' => [
'$collection' => Database::METADATA,
'$id' => 'collections',
'name' => 'Collections',
'$id' => 'databases',
'name' => 'Databases',
'attributes' => [
[
'$id' => 'name',
@@ -31,27 +31,63 @@ $collections = [
'filters' => [],
],
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 0,
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => 'dateUpdated',
'type' => Database::VAR_INTEGER,
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
'collections' => [
'$collection' => 'databases',
'$id' => 'collections',
'name' => 'Collections',
'attributes' => [
[
'$id' => 'databaseInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 0,
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'databaseId',
'type' => Database::VAR_STRING,
'signed' => true,
'size' => Database::LENGTH_KEY,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'size' => 256,
'required' => true,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'enabled',
'type' => Database::VAR_BOOLEAN,
@@ -120,7 +156,7 @@ $collections = [
'name' => 'Attributes',
'attributes' => [
[
'$id' => 'collectionId',
'$id' => 'databaseInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -130,6 +166,39 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'databaseId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'collectionInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'collectionId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'key',
'type' => Database::VAR_STRING,
@@ -249,11 +318,11 @@ $collections = [
],
'indexes' => [
[
'$id' => '_key_collection',
'$id' => '_key_db_collection',
'type' => Database::INDEX_KEY,
'attributes' => ['collectionId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
'attributes' => ['databaseInternalId', 'collectionInternalId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
],
],
@@ -264,7 +333,7 @@ $collections = [
'name' => 'Indexes',
'attributes' => [
[
'$id' => 'collectionId',
'$id' => 'databaseInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -274,6 +343,39 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'databaseId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'collectionInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'collectionId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'key',
'type' => Database::VAR_STRING,
@@ -343,11 +445,11 @@ $collections = [
],
'indexes' => [
[
'$id' => '_key_collection',
'$id' => '_key_db_collection',
'type' => Database::INDEX_KEY,
'attributes' => ['collectionId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
'attributes' => ['databaseInternalId', 'collectionInternalId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
],
],
@@ -357,6 +459,17 @@ $collections = [
'$id' => 'projects',
'name' => 'Projects',
'attributes' => [
[
'$id' => 'teamInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'teamId',
'type' => Database::VAR_STRING,
@@ -594,6 +707,17 @@ $collections = [
'$id' => 'platforms',
'name' => 'platforms',
'attributes' => [
[
'$id' => 'projectInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
@@ -659,35 +783,13 @@ $collections = [
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateUpdated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
]
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -699,6 +801,17 @@ $collections = [
'$id' => 'domains',
'name' => 'domains',
'attributes' => [
[
'$id' => 'projectInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
@@ -781,7 +894,7 @@ $collections = [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -794,7 +907,7 @@ $collections = [
'name' => 'keys',
'attributes' => [
[
'$id' => 'projectId',
'$id' => 'projectInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -804,6 +917,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
@@ -837,12 +961,23 @@ $collections = [
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'expire',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -854,6 +989,17 @@ $collections = [
'$id' => 'webhooks',
'name' => 'webhooks',
'attributes' => [
[
'$id' => 'projectInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
@@ -947,10 +1093,10 @@ $collections = [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
]
],
],
@@ -981,6 +1127,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'phone',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16, // leading '+' and 15 digitts maximum by E.164 format
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'status',
'type' => Database::VAR_BOOLEAN,
@@ -1047,6 +1204,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'phoneVerification',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'reset',
'type' => Database::VAR_BOOLEAN,
@@ -1111,6 +1279,13 @@ $collections = [
'lengths' => [320],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_phone',
'type' => Database::INDEX_UNIQUE,
'attributes' => ['phone'],
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
@@ -1126,6 +1301,17 @@ $collections = [
'$id' => 'tokens',
'name' => 'Tokens',
'attributes' => [
[
'$id' => 'userInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'userId',
'type' => Database::VAR_STRING,
@@ -1197,7 +1383,7 @@ $collections = [
[
'$id' => '_key_user',
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'attributes' => ['userInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -1209,6 +1395,17 @@ $collections = [
'$id' => 'sessions',
'name' => 'Sessions',
'attributes' => [
[
'$id' => 'userInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'userId',
'type' => Database::VAR_STRING,
@@ -1474,7 +1671,7 @@ $collections = [
[
'$id' => '_key_user',
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'attributes' => ['userInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -1497,17 +1694,6 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'total',
'type' => Database::VAR_INTEGER,
@@ -1548,7 +1734,7 @@ $collections = [
'name' => 'Memberships',
'attributes' => [
[
'$id' => 'teamId',
'$id' => 'userInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -1569,6 +1755,28 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'teamInternalId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'teamId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'roles',
'type' => Database::VAR_STRING,
@@ -1640,21 +1848,21 @@ $collections = [
[
'$id' => '_key_unique',
'type' => Database::INDEX_UNIQUE,
'attributes' => ['teamId', 'userId'],
'attributes' => ['teamInternalId', 'userInternalId'],
'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => '_key_team',
'$id' => '_key_user',
'type' => Database::INDEX_KEY,
'attributes' => ['teamId'],
'attributes' => ['userInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_user',
'$id' => '_key_team',
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'attributes' => ['teamInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
@@ -1695,28 +1903,6 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateUpdated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'array' => false,
'$id' => 'status',
@@ -1726,7 +1912,6 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
@@ -1845,17 +2030,6 @@ $collections = [
'$id' => 'deployments',
'name' => 'Deployments',
'attributes' => [
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'resourceId',
'type' => Database::VAR_STRING,
@@ -1898,7 +2072,6 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
@@ -2147,17 +2320,6 @@ $collections = [
'$id' => 'executions',
'name' => 'Executions',
'attributes' => [
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'functionId',
'type' => Database::VAR_STRING,
@@ -2189,7 +2351,6 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
@@ -2368,26 +2529,6 @@ $collections = [
'$id' => 'buckets',
'name' => 'Buckets',
'attributes' => [
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'signed' => false,
'size' => 0,
'required' => false,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateUpdated',
'type' => Database::VAR_INTEGER,
'size' => 0,
'format' => '',
'signed' => false,
'required' => false,
'array' => false,
'filters' => [],
],
[
'$id' => 'enabled',
'type' => Database::VAR_BOOLEAN,
@@ -2628,17 +2769,6 @@ $collections = [
'$id' => 'files',
'$name' => 'Files',
'attributes' => [
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'array' => false,
'$id' => 'bucketId',
@@ -2648,7 +2778,6 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
+15
View File
@@ -48,6 +48,11 @@ return [
'description' => 'SMTP is disabled on your Appwrite instance. You can <a href="/docs/email-delivery">learn more about setting up SMTP</a> in our docs.',
'code' => 503,
],
Exception::GENERAL_PHONE_DISABLED => [
'name' => Exception::GENERAL_PHONE_DISABLED,
'description' => 'Phone provider is not configured. Please check the _APP_PHONE_PROVIDER environment variable of your Appwrite server.',
'code' => 503,
],
Exception::GENERAL_ARGUMENT_INVALID => [
'name' => Exception::GENERAL_ARGUMENT_INVALID,
'description' => 'The request contains one or more invalid arguments. Please refer to the endpoint documentation.',
@@ -170,6 +175,16 @@ return [
'description' => 'The requested authentication method is either disabled or unsupported. Please check the supported authentication methods in the Appwrite console.',
'code' => 501,
],
Exception::USER_PHONE_ALREADY_EXISTS => [
'name' => Exception::USER_PHONE_ALREADY_EXISTS,
'description' => 'A user with the same phone number already exists in the current project.',
'code' => 409,
],
Exception::USER_PHONE_NOT_FOUND => [
'name' => Exception::USER_PHONE_NOT_FOUND,
'description' => 'The current user does not have a phone number associated with their account.',
'code' => 400,
],
/** Teams */
Exception::TEAM_NOT_FOUND => [
+48 -34
View File
@@ -69,54 +69,68 @@ return [
],
]
],
'collections' => [
'$model' => Response::MODEL_COLLECTION,
'databases' => [
'$model' => Response::MODEL_DATABASE,
'$resource' => true,
'$description' => 'This event triggers on any collection event.',
'documents' => [
'$model' => Response::MODEL_DOCUMENT,
'$description' => 'This event triggers on any database event.',
'collections' => [
'$model' => Response::MODEL_COLLECTION,
'$resource' => true,
'$description' => 'This event triggers on any documents event.',
'$description' => 'This event triggers on any collection event.',
'documents' => [
'$model' => Response::MODEL_DOCUMENT,
'$resource' => true,
'$description' => 'This event triggers on any documents event.',
'create' => [
'$description' => 'This event triggers when a document is created.',
],
'delete' => [
'$description' => 'This event triggers when a document is deleted.'
],
'update' => [
'$description' => 'This event triggers when a document is updated.'
],
],
'indexes' => [
'$model' => Response::MODEL_INDEX,
'$resource' => true,
'$description' => 'This event triggers on any indexes event.',
'create' => [
'$description' => 'This event triggers when an index is created.',
],
'delete' => [
'$description' => 'This event triggers when an index is deleted.'
]
],
'attributes' => [
'$model' => Response::MODEL_ATTRIBUTE,
'$resource' => true,
'$description' => 'This event triggers on any attributes event.',
'create' => [
'$description' => 'This event triggers when an attribute is created.',
],
'delete' => [
'$description' => 'This event triggers when an attribute is deleted.'
]
],
'create' => [
'$description' => 'This event triggers when a document is created.',
'$description' => 'This event triggers when a collection is created.'
],
'delete' => [
'$description' => 'This event triggers when a document is deleted.'
'$description' => 'This event triggers when a collection is deleted.',
],
'update' => [
'$description' => 'This event triggers when a document is updated.'
],
],
'indexes' => [
'$model' => Response::MODEL_INDEX,
'$resource' => true,
'$description' => 'This event triggers on any indexes event.',
'create' => [
'$description' => 'This event triggers when an index is created.',
],
'delete' => [
'$description' => 'This event triggers when an index is deleted.'
]
],
'attributes' => [
'$model' => Response::MODEL_ATTRIBUTE,
'$resource' => true,
'$description' => 'This event triggers on any attributes event.',
'create' => [
'$description' => 'This event triggers when an attribute is created.',
],
'delete' => [
'$description' => 'This event triggers when an attribute is deleted.'
'$description' => 'This event triggers when a collection is updated.',
]
],
'create' => [
'$description' => 'This event triggers when a collection is created.'
'$description' => 'This event triggers when a database is created.'
],
'delete' => [
'$description' => 'This event triggers when a collection is deleted.',
'$description' => 'This event triggers when a database is deleted.',
],
'update' => [
'$description' => 'This event triggers when a collection is updated.',
'$description' => 'This event triggers when a database is updated.',
]
],
'buckets' => [
+14 -14
View File
@@ -15,7 +15,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '8.0.1',
'version' => '9.0.1',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -63,7 +63,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '5.0.0',
'version' => '6.0.0',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -81,7 +81,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '0.6.1',
'version' => '0.7.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -162,7 +162,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '5.0.0',
'version' => '6.0.0',
'url' => 'https://github.com/appwrite/sdk-for-console',
'package' => '',
'enabled' => true,
@@ -180,7 +180,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '0.17.1',
'version' => '0.18.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -208,7 +208,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '6.0.0',
'version' => '7.0.2',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -226,7 +226,7 @@ return [
[
'key' => 'deno',
'name' => 'Deno',
'version' => '4.0.0',
'version' => '5.0.1',
'url' => 'https://github.com/appwrite/sdk-for-deno',
'package' => 'https://deno.land/x/appwrite',
'enabled' => true,
@@ -244,7 +244,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '5.0.0',
'version' => '6.0.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -262,7 +262,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '0.9.0',
'version' => '0.10.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@@ -280,7 +280,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '5.0.0',
'version' => '6.0.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@@ -352,7 +352,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '5.0.1',
'version' => '6.0.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@@ -370,7 +370,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@@ -392,7 +392,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
+31 -10
View File
@@ -31,6 +31,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'autodesk' => [
'name' => 'Autodesk',
'developers' => 'https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/',
'icon' => 'icon-autodesk',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
'bitbucket' => [
'name' => 'BitBucket',
'developers' => 'https://developer.atlassian.com/bitbucket',
@@ -61,6 +71,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false
],
'dailymotion' => [
'name' => 'Dailymotion',
'developers' => 'https://developers.dailymotion.com/api/',
'icon' => 'icon-dailymotion',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
'discord' => [
'name' => 'Discord',
'developers' => 'https://discordapp.com/developers/docs/topics/oauth2',
@@ -291,6 +311,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'zoom' => [
'name' => 'Zoom',
'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
'icon' => 'icon-zoom',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
// 'instagram' => [
// 'name' => 'Instagram',
// 'developers' => 'https://www.instagram.com/developer/',
@@ -307,16 +337,7 @@ return [ // Ordered by ABC.
// 'beta' => false,
// 'mock' => false,
// ],
'zoom' => [
'name' => 'Zoom',
'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/',
'icon' => 'icon-zoom',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
],
// Keep Last
'mock' => [
'name' => 'Mock',
+2
View File
@@ -34,6 +34,8 @@ $admins = [
'buckets.write',
'users.read',
'users.write',
'databases.read',
'databases.write',
'collections.read',
'collections.write',
'platforms.read',
+6
View File
@@ -13,6 +13,12 @@ return [ // List of publicly visible scopes
'teams.write' => [
'description' => 'Access to create, update, and delete your project\'s teams',
],
'databases.read' => [
'description' => 'Access to read your project\'s databases',
],
'databases.write' => [
'description' => 'Access to create, update, and delete your project\'s databases',
],
'collections.read' => [
'description' => 'Access to read your project\'s database collections',
],
+11 -8
View File
@@ -53,18 +53,21 @@ return [
'optional' => true,
'icon' => '/images/services/avatars.png',
],
'database' => [
'key' => 'database',
'name' => 'Database',
'subtitle' => 'The Database service allows you to create structured collections of documents, query and filter lists of documents',
'description' => '/docs/services/database.md',
'controller' => 'api/database.php',
'databases' => [
'key' => 'databases',
'name' => 'Databases',
'subtitle' => 'The Databases service allows you to create structured collections of documents, query and filter lists of documents',
'description' => '/docs/services/databases.md',
'controller' => 'api/databases.php',
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/database',
'docsUrl' => 'https://appwrite.io/docs/client/databases',
'tests' => false,
'optional' => true,
'icon' => '/images/services/database.png',
'icon' => '/images/services/databases.png',
'globalAttributes' => [
'databaseId'
]
],
'locale' => [
'key' => 'locale',
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
+27 -3
View File
@@ -98,7 +98,7 @@ return [
// ],
[
'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.',
'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.",
'introduction' => '',
'default' => '',
'required' => false,
@@ -340,7 +340,7 @@ return [
],
[
'category' => 'SMTP',
'description' => 'Appwrite is using an SMTP server for emailing your projects users and server admins. The SMTP env vars are used to allow Appwrite server to connect to the SMTP container.\n\nIf running in production, it might be easier to use a 3rd party SMTP server as it might be a little more difficult to set up a production SMTP server that will not send all your emails into your user\'s SPAM folder.',
'description' => "Appwrite is using an SMTP server for emailing your projects users and server admins. The SMTP env vars are used to allow Appwrite server to connect to the SMTP container.\n\nIf running in production, it might be easier to use a 3rd party SMTP server as it might be a little more difficult to set up a production SMTP server that will not send all your emails into your user\'s SPAM folder.",
'variables' => [
[
'name' => '_APP_SMTP_HOST',
@@ -389,6 +389,30 @@ return [
],
],
],
[
'category' => 'Phone',
'description' => '',
'variables' => [
[
'name' => '_APP_PHONE_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'phone://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_PHONE_FROM',
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
'introduction' => '0.15.0',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
],
],
[
'category' => 'Storage',
'description' => '',
@@ -677,7 +701,7 @@ return [
],
[
'name' => '_APP_FUNCTIONS_RUNTIMES',
'description' => "This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\n\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\n\nCurrently, supported environments are: " . \implode(', ', \array_keys(Config::getParam('runtimes'))),
'description' => "This option allows you to enable or disable runtime environments for cloud functions. Disable unused runtimes to save disk space.\n\nTo enable cloud function runtimes, pass a list of enabled environments separated by a comma.\n\nCurrently, supported environments are: " . \implode(', ', \array_keys(Config::getParam('runtimes'))),
'introduction' => '0.8.0',
'default' => 'node-16.0,php-8.0,python-3.9,ruby-3.0',
'required' => false,
+456 -21
View File
@@ -2,7 +2,9 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone as ValidatorPhone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
@@ -19,6 +21,7 @@ use Appwrite\Utopia\Database\Validator\CustomId;
use MaxMind\Db\Reader;
use Utopia\App;
use Appwrite\Event\Audit;
use Appwrite\Event\Phone as EventPhone;
use Utopia\Audit\Audit as EventAudit;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -129,16 +132,16 @@ App::post('/v1/account')
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/account/sessions')
->desc('Create Account Session')
App::post('/v1/account/sessions/email')
->desc('Create Account Session with Email')
->groups(['api', 'account', 'auth'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'public')
->label('auth.type', 'emailPassword')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createSession')
->label('sdk.description', '/docs/references/account/create-session.md')
->label('sdk.method', 'createEmailSession')
->label('sdk.description', '/docs/references/account/create-session-email.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
@@ -178,6 +181,7 @@ App::post('/v1/account/sessions')
[
'$id' => $dbForProject->getId(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $email,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
@@ -254,7 +258,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), fn($node) => (!$node['mock'])))) . '.')
->param('success', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a successful login attempt. 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'])
->param('failure', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a failed login attempt. 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'])
->param('scopes', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 128 characters long.', true)
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('request')
->inject('response')
->inject('project')
@@ -507,6 +511,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => $provider,
'providerUid' => $oauth2ID,
'providerAccessToken' => $accessToken,
@@ -519,7 +524,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password'));
$isAnonymousUser = Auth::isAnonymousUser($user);
if ($isAnonymousUser) {
$user
@@ -661,6 +666,7 @@ App::post('/v1/account/sessions/magic-url')
$token = new Document([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
'secret' => Auth::hash($loginSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
@@ -738,6 +744,8 @@ App::put('/v1/account/sessions/magic-url')
->inject('events')
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) {
/** @var Utopia\Database\Document $user */
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($user->isEmpty()) {
@@ -758,6 +766,7 @@ App::put('/v1/account/sessions/magic-url')
[
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_MAGIC_URL,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
@@ -824,6 +833,234 @@ App::put('/v1/account/sessions/magic-url')
$response->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/account/sessions/phone')
->desc('Create Phone session')
->groups(['api', 'account'])
->label('scope', 'public')
->label('auth.type', 'phone')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPhoneSession')
->label('sdk.description', '/docs/references/account/create-phone-session.md')
->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}')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. 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('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('audits')
->inject('events')
->inject('messaging')
->inject('phone')
->action(function (string $userId, string $number, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, EventPhone $messaging, Phone $phone) {
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
throw new Exception('Phone provider not configured', 503, Exception::GENERAL_PHONE_DISABLED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$user = $dbForProject->findOne('users', [new Query('phone', Query::TYPE_EQUAL, [$number])]);
if (!$user) {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
}
}
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => null,
'phone' => $number,
'emailVerification' => false,
'phoneVerification' => false,
'status' => true,
'password' => null,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $number])
])));
}
$secret = $phone->generateSecretDigits();
$expire = \time() + Auth::TOKEN_EXPIRATION_PHONE;
$token = new Document([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
'secret' => $secret,
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$dbForProject->deleteCachedDocument('users', $user->getId());
$messaging
->setRecipient($number)
->setMessage($secret)
->trigger();
$events->setPayload(
$response->output(
$token->setAttribute('secret', $secret),
Response::MODEL_TOKEN
)
);
// Hide secret for clients
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
$audits
->setResource('user/' . $user->getId())
->setUser($user)
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN)
;
});
App::put('/v1/account/sessions/phone')
->desc('Create Phone session (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhoneSession')
->label('sdk.description', '/docs/references/account/update-phone-session.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new CustomId(), 'User ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audits')
->inject('events')
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) {
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$token = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
if (!$token) {
throw new Exception('Invalid login token', 401, Exception::USER_INVALID_TOKEN);
}
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_PHONE,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
Authorization::setRole('user:' . $user->getId());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$dbForProject->deleteCachedDocument('users', $user->getId());
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $token);
$dbForProject->deleteCachedDocument('users', $user->getId());
$user->setAttribute('phoneVerification', true);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
if (false === $user) {
throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR);
}
$audits->setResource('user/' . $user->getId());
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
$protocol = $request->getProtocol();
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
;
$response->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/account/sessions/anonymous')
->desc('Create Anonymous Session')
->groups(['api', 'account', 'auth'])
@@ -901,6 +1138,7 @@ App::post('/v1/account/sessions/anonymous')
[
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
@@ -965,7 +1203,7 @@ App::post('/v1/account/jwt')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_JWT)
->label('abuse-limit', 10)
->label('abuse-limit', 100)
->label('abuse-key', 'url:{url},userId:{userId}')
->inject('response')
->inject('user')
@@ -1285,7 +1523,7 @@ App::patch('/v1/account/email')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
!$isAnonymousUser &&
@@ -1295,18 +1533,15 @@ App::patch('/v1/account/email')
}
$email = \strtolower($email);
$profile = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if ($profile) {
throw new Exception('User already registered', 409, Exception::USER_ALREADY_EXISTS);
}
$user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')])));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
} catch (Duplicate $th) {
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
}
@@ -1322,6 +1557,59 @@ App::patch('/v1/account/email')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/account/phone')
->desc('Update Account Phone')
->groups(['api', 'account'])
->label('event', 'users.[userId].update.phone')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhone')
->label('sdk.description', '/docs/references/account/update-phone.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
!$isAnonymousUser &&
!Auth::passwordVerify($password, $user->getAttribute('password'))
) { // Double check user password
throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS);
}
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
} catch (Duplicate $th) {
throw new Exception('Phone number already exists', 409, Exception::USER_PHONE_ALREADY_EXISTS);
}
$audits
->setResource('user/' . $user->getId())
->setUser($user)
;
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/account/prefs')
->desc('Update Account Preferences')
->groups(['api', 'account'])
@@ -1530,8 +1818,7 @@ App::patch('/v1/account/sessions/:sessionId')
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
->setAttribute('providerAccessTokenExpiry', \time() + (int) $oauth2->getAccessTokenExpiry(''))
;
->setAttribute('providerAccessTokenExpiry', \time() + (int) $oauth2->getAccessTokenExpiry(''));
$dbForProject->updateDocument('sessions', $sessionId, $session);
@@ -1680,6 +1967,7 @@ App::post('/v1/account/recovery')
$recovery = new Document([
'$id' => $dbForProject->getId(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expire,
@@ -1806,7 +2094,7 @@ App::post('/v1/account/verification')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createVerification')
->label('sdk.description', '/docs/references/account/create-verification.md')
->label('sdk.description', '/docs/references/account/create-email-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
@@ -1840,6 +2128,7 @@ App::post('/v1/account/verification')
$verification = new Document([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_VERIFICATION,
'secret' => Auth::hash($verificationSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
@@ -1895,7 +2184,7 @@ App::put('/v1/account/verification')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateVerification')
->label('sdk.description', '/docs/references/account/update-verification.md')
->label('sdk.description', '/docs/references/account/update-email-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
@@ -1948,3 +2237,149 @@ App::put('/v1/account/verification')
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});
App::post('/v1/account/verification/phone')
->desc('Create Phone Verification')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'users.[userId].verification.[tokenId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPhoneVerification')
->label('sdk.description', '/docs/references/account/create-phone-verification.md')
->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', 'userId:{userId}')
->inject('request')
->inject('response')
->inject('phone')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('events')
->inject('usage')
->inject('messaging')
->action(function (Request $request, Response $response, Phone $phone, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage, EventPhone $messaging) {
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
throw new Exception('Phone provider not configured', 503, Exception::GENERAL_PHONE_DISABLED);
}
if (empty($user->getAttribute('phone'))) {
throw new Exception('User has no phone number.', 400, Exception::USER_PHONE_NOT_FOUND);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$verificationSecret = Auth::tokenGenerator();
$secret = $phone->generateSecretDigits();
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
$verification = new Document([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
'secret' => $secret,
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$dbForProject->deleteCachedDocument('users', $user->getId());
$messaging
->setRecipient($user->getAttribute('phone'))
->setMessage($secret)
->trigger()
;
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
->setPayload($response->output(
$verification->setAttribute('secret', $verificationSecret),
Response::MODEL_TOKEN
))
;
// Hide secret for clients
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
$audits->setResource('user/' . $user->getId());
$usage->setParam('users.update', 1);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($verification, Response::MODEL_TOKEN);
});
App::put('/v1/account/verification/phone')
->desc('Create Phone Verification (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].verification.[tokenId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhoneVerification')
->label('sdk.description', '/docs/references/account/update-phone-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{param-userId}')
->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($profile->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$verification = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
if (!$verification) {
throw new Exception('Invalid verification token', 401, Exception::USER_INVALID_TOKEN);
}
Authorization::setRole('user:' . $profile->getId());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
/**
* We act like we're updating and validating the verification token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteCachedDocument('users', $profile->getId());
$audits->setResource('user/' . $user->getId());
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verificationDocument->getId())
;
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});
File diff suppressed because it is too large Load Diff
+6 -11
View File
@@ -66,8 +66,6 @@ App::post('/v1/functions')
$function = $dbForProject->createDocument('functions', new Document([
'$id' => $functionId,
'execute' => $execute,
'dateCreated' => time(),
'dateUpdated' => time(),
'status' => 'disabled',
'name' => $name,
'runtime' => $runtime,
@@ -102,7 +100,7 @@ App::get('/v1/functions')
->param('limit', 25, new Range(0, 100), 'Maximum number of functions to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the function used as the starting point for the query, excluding the function itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -315,7 +313,6 @@ App::put('/v1/functions/:functionId')
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'execute' => $execute,
'dateUpdated' => time(),
'name' => $name,
'vars' => $vars,
'events' => $events,
@@ -575,7 +572,6 @@ App::post('/v1/functions/:functionId/deployments')
'$write' => ['role:all'],
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'dateCreated' => time(),
'entrypoint' => $entrypoint,
'path' => $path,
'size' => $fileSize,
@@ -605,7 +601,6 @@ App::post('/v1/functions/:functionId/deployments')
'$write' => ['role:all'],
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'dateCreated' => time(),
'entrypoint' => $entrypoint,
'path' => $path,
'size' => $fileSize,
@@ -646,7 +641,7 @@ App::get('/v1/functions/:functionId/deployments')
->param('limit', 25, new Range(0, 100), 'Maximum number of deployments to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the deployment used as the starting point for the query, excluding the deployment itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -854,11 +849,11 @@ App::post('/v1/functions/:functionId/executions')
$executionId = $dbForProject->getId();
/** @var Document $execution */
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [],
'$write' => [],
'dateCreated' => time(),
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId(),
'trigger' => 'http', // http / schedule / event
@@ -894,7 +889,7 @@ App::post('/v1/functions/:functionId/executions')
$events
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setContext($function);
->setContext('function', $function);
if ($async) {
$event = new Func();
@@ -952,7 +947,7 @@ App::post('/v1/functions/:functionId/executions')
$execution->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
$endtime = \microtime(true);
$time = $endtime - $execution->getAttribute('dateCreated');
$time = $endtime - $execution->getCreatedAt();
$execution->setAttribute('time', $time);
$execution->setAttribute('status', 'failed');
$execution->setAttribute('statusCode', $th->getCode());
@@ -983,7 +978,7 @@ App::get('/v1/functions/:functionId/executions')
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('cursor', '', new UID(), 'ID of the execution used as the starting point for the query, excluding the execution itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $functionId, int $limit, int $offset, string $search, string $cursor, string $cursorDirection, Response $response, Database $dbForProject) {
+77 -36
View File
@@ -26,6 +26,7 @@ use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -77,14 +78,17 @@ App::post('/v1/projects')
}
$projectId = ($projectId == 'unique()') ? $dbForConsole->getId() : $projectId;
if ($projectId === 'console') {
throw new Exception("'console' is a reserved project.", 400, Exception::PROJECT_RESERVED_PROJECT);
}
$project = $dbForConsole->createDocument('projects', new Document([
'$id' => $projectId == 'unique()' ? $dbForConsole->getId() : $projectId,
'$id' => $projectId,
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner', 'team:' . $teamId . '/developer'],
'name' => $name,
'teamInternalId' => $team->getInternalId(),
'teamId' => $team->getId(),
'description' => $description,
'logo' => $logo,
@@ -108,7 +112,7 @@ App::post('/v1/projects')
/** @var array $collections */
$collections = Config::getParam('collections', []);
$dbForProject->setNamespace("_{$project->getId()}");
$dbForProject->setNamespace("_{$project->getInternalId()}");
$dbForProject->create('appwrite');
$audit = new Audit($dbForProject);
@@ -169,7 +173,7 @@ App::get('/v1/projects')
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the project used as the starting point for the query, excluding the project itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForConsole')
@@ -267,15 +271,15 @@ App::get('/v1/projects/:projectId/usage')
],
];
$dbForProject->setNamespace("_{$projectId}");
$dbForProject->setNamespace("_{$project->getInternalId()}");
$metrics = [
'requests',
'network',
'executions',
'users.count',
'database.documents.count',
'database.collections.count',
'databases.documents.count',
'databases.collections.count',
'storage.total'
];
@@ -322,8 +326,8 @@ App::get('/v1/projects/:projectId/usage')
'requests' => $stats['requests'],
'network' => $stats['network'],
'functions' => $stats['executions'],
'documents' => $stats['database.documents.count'],
'collections' => $stats['database.collections.count'],
'documents' => $stats['databases.documents.count'],
'collections' => $stats['databases.collections.count'],
'users' => $stats['users.count'],
'storage' => $stats['storage.total']
]);
@@ -582,12 +586,11 @@ App::post('/v1/projects/:projectId/webhooks')
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
'events' => $events,
@@ -628,7 +631,7 @@ App::get('/v1/projects/:projectId/webhooks')
}
$webhooks = $dbForConsole->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
], 5000);
$response->dynamic(new Document([
@@ -661,7 +664,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($webhook === false || $webhook->isEmpty()) {
@@ -689,10 +692,9 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('signatureKey', null, new Text(256), 'Webhook signature key. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, string $signatureKey, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $webhookId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -704,7 +706,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($webhook === false || $webhook->isEmpty()) {
@@ -720,10 +722,45 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->setAttribute('httpPass', $httpPass)
;
if (!empty($signatureKey)) {
$webhook->setAttribute('signatureKey', $signatureKey);
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
});
App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
->desc('Update Webhook Signature Key')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateWebhookSignature')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_WEBHOOK)
->param('projectId', null, new UID(), 'Project unique ID.')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
}
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
}
$webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
@@ -753,7 +790,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($webhook === false || $webhook->isEmpty()) {
@@ -782,9 +819,10 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -796,9 +834,11 @@ App::post('/v1/projects/:projectId/keys')
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
'secret' => \bin2hex(\random_bytes(128)),
]);
@@ -832,7 +872,7 @@ App::get('/v1/projects/:projectId/keys')
}
$keys = $dbForConsole->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()]),
], 5000);
$response->dynamic(new Document([
@@ -865,7 +905,7 @@ App::get('/v1/projects/:projectId/keys/:keyId')
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($key === false || $key->isEmpty()) {
@@ -889,9 +929,10 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', null, new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $keyId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -901,7 +942,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($key === false || $key->isEmpty()) {
@@ -911,6 +952,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
->setAttribute('expire', $expire)
;
$dbForConsole->updateDocument('keys', $key->getId(), $key);
@@ -943,7 +985,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($key === false || $key->isEmpty()) {
@@ -988,14 +1030,13 @@ App::post('/v1/projects/:projectId/platforms')
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'type' => $type,
'name' => $name,
'key' => $key,
'store' => $store,
'hostname' => $hostname,
'dateCreated' => \time(),
'dateUpdated' => \time(),
'hostname' => $hostname
]);
$platform = $dbForConsole->createDocument('platforms', $platform);
@@ -1061,7 +1102,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($platform === false || $platform->isEmpty()) {
@@ -1098,7 +1139,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($platform === false || $platform->isEmpty()) {
@@ -1107,7 +1148,6 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
$platform
->setAttribute('name', $name)
->setAttribute('dateUpdated', \time())
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname)
@@ -1143,7 +1183,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($platform === false || $platform->isEmpty()) {
@@ -1183,7 +1223,7 @@ App::post('/v1/projects/:projectId/domains')
$document = $dbForConsole->findOne('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()]),
]);
if ($document && !$document->isEmpty()) {
@@ -1202,6 +1242,7 @@ App::post('/v1/projects/:projectId/domains')
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'updated' => \time(),
'domain' => $domain->get(),
@@ -1241,7 +1282,7 @@ App::get('/v1/projects/:projectId/domains')
}
$domains = $dbForConsole->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
], 5000);
$response->dynamic(new Document([
@@ -1274,7 +1315,7 @@ App::get('/v1/projects/:projectId/domains/:domainId')
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($domain === false || $domain->isEmpty()) {
@@ -1308,7 +1349,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($domain === false || $domain->isEmpty()) {
@@ -1368,7 +1409,7 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
]);
if ($domain === false || $domain->isEmpty()) {
+5 -9
View File
@@ -107,8 +107,6 @@ App::post('/v1/storage/buckets')
$bucket = $dbForProject->createDocument('buckets', new Document([
'$id' => $bucketId,
'$collection' => 'buckets',
'dateCreated' => \time(),
'dateUpdated' => \time(),
'name' => $name,
'permission' => $permission,
'maximumFileSize' => $maximumFileSize,
@@ -158,7 +156,7 @@ App::get('/v1/storage/buckets')
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the bucket used as the starting point for the query, excluding the bucket itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -546,7 +544,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
'$id' => $fileId,
'$read' => $read,
'$write' => $write,
'dateCreated' => \time(),
'bucketId' => $bucket->getId(),
'name' => $fileName,
'path' => $path,
@@ -613,7 +610,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
'$id' => $fileId,
'$read' => $read,
'$write' => $write,
'dateCreated' => \time(),
'bucketId' => $bucket->getId(),
'name' => $fileName,
'path' => $path,
@@ -654,7 +650,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
->setContext('bucket', $bucket)
;
$metadata = null; // was causing leaks as it was passed by reference
@@ -680,7 +676,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
->param('limit', 25, new Range(0, 100), 'Maximum number of files to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the file used as the starting point for the query, excluding the file itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -1361,7 +1357,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
->setContext('bucket', $bucket)
;
$audits->setResource('file/' . $file->getId());
@@ -1463,7 +1459,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
->setContext('bucket', $bucket)
->setPayload($response->output($file, Response::MODEL_FILE))
;
+7 -3
View File
@@ -63,7 +63,6 @@ App::post('/v1/teams')
'$write' => ['team:' . $teamId . '/owner'],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'dateCreated' => \time(),
'search' => implode(' ', [$teamId, $name]),
])));
@@ -74,7 +73,9 @@ App::post('/v1/teams')
'$read' => ['user:' . $user->getId(), 'team:' . $team->getId()],
'$write' => ['user:' . $user->getId(), 'team:' . $team->getId() . '/owner'],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => \time(),
'joined' => \time(),
@@ -118,7 +119,7 @@ App::get('/v1/teams')
->param('limit', 25, new Range(0, 100), 'Maximum number of teams to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the team used as the starting point for the query, excluding the team itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -367,7 +368,9 @@ App::post('/v1/teams/:teamId/memberships')
'$read' => ['role:all'],
'$write' => ['user:' . $invitee->getId(), 'team:' . $team->getId() . '/owner'],
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => \time(),
'joined' => ($isPrivilegedUser || $isAppUser) ? \time() : 0,
@@ -446,7 +449,7 @@ App::get('/v1/teams/:teamId/memberships')
->param('limit', 25, new Range(0, 100), 'Maximum number of memberships to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the membership used as the starting point for the query, excluding the membership itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -702,6 +705,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
+93 -8
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@@ -103,7 +104,7 @@ App::get('/v1/users')
->param('limit', 25, new Range(0, 100), 'Maximum number of users to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the user used as the starting point for the query, excluding the user itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
@@ -406,8 +407,8 @@ App::patch('/v1/users/:userId/verification')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateVerification')
->label('sdk.description', '/docs/references/users/update-user-verification.md')
->label('sdk.method', 'updateEmailVerification')
->label('sdk.description', '/docs/references/users/update-user-email-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
@@ -438,6 +439,45 @@ App::patch('/v1/users/:userId/verification')
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification/phone')
->desc('Update Phone Verification')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhoneVerification')
->label('sdk.description', '/docs/references/users/update-user-phone-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User ID.')
->param('phoneVerification', false, new Boolean(), 'User phone verification status.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $phoneVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/name')
->desc('Update Name')
->groups(['api', 'users'])
@@ -551,15 +591,11 @@ App::patch('/v1/users/:userId/email')
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
if (!$isAnonymousUser) {
//TODO: Remove previous unique ID.
}
$email = \strtolower($email);
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name')]))
;
@@ -570,6 +606,55 @@ App::patch('/v1/users/:userId/email')
}
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/phone')
->desc('Update Phone')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.phone')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhone')
->label('sdk.description', '/docs/references/users/update-user-phone.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User ID.')
->param('number', '', new Phone(), 'User phone number.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $userId, string $number, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
} catch (Duplicate $th) {
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
}
$audits
->setResource('user/' . $user->getId())
;
+11 -2
View File
@@ -19,6 +19,7 @@ use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
use Appwrite\Utopia\Response\Filters\V13 as ResponseV13;
use Appwrite\Utopia\Response\Filters\V14 as ResponseV14;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -177,6 +178,9 @@ App::init(function (App $utopia, Request $request, Response $response, Document
case version_compare($responseFormat, '0.13.4', '<='):
Response::setFilter(new ResponseV13());
break;
case version_compare($responseFormat, '0.14.0', '<='):
Response::setFilter(new ResponseV14());
break;
default:
Response::setFilter(null);
}
@@ -279,6 +283,12 @@ App::init(function (App $utopia, Request $request, Response $response, Document
$role = Auth::USER_ROLE_APP;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire', 0);
if (!empty($expire) && $expire < \time()) {
throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED);
}
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
}
@@ -292,11 +302,10 @@ App::init(function (App $utopia, Request $request, Response $response, Document
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
$serviceRoles = Authorization::getRoles();
if (
array_key_exists($service, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser($serviceRoles) || Auth::isAppUser($serviceRoles))
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
) {
throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED);
}
+7 -5
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Auth;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
@@ -18,7 +19,7 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
App::init(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, Event $database, Database $dbForProject, string $mode) {
App::init(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $events, Audit $audits, Mail $mails, Stats $usage, Delete $deletes, EventDatabase $database, Database $dbForProject, string $mode) {
$route = $utopia->match($request);
@@ -164,7 +165,7 @@ App::init(function (App $utopia, Request $request, Document $project) {
}
}, ['utopia', 'request', 'project'], 'auth');
App::shutdown(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, Event $database, string $mode, Database $dbForProject) {
App::shutdown(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) {
if (!empty($events->getEvent())) {
if (empty($events->getPayload())) {
@@ -192,16 +193,17 @@ App::shutdown(function (App $utopia, Request $request, Response $response, Docum
if ($project->getId() !== 'console') {
$allEvents = Event::generateEvents($events->getEvent(), $events->getParams());
$payload = new Document($events->getPayload());
$context = $events->getContext() ?? false;
$collection = ($context && $context->getCollection() === 'collections') ? $context : null;
$bucket = ($context && $context->getCollection() === 'buckets') ? $context : null;
$db = $events->getContext('database');
$collection = $events->getContext('collection');
$bucket = $events->getContext('bucket');
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $project,
database: $db,
collection: $collection,
bucket: $bucket,
);
+53 -12
View File
@@ -215,21 +215,56 @@ App::get('/console/keys')
->setParam('body', $page);
});
App::get('/console/database')
App::get('/console/databases')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/console/database/index.phtml');
$page = new View(__DIR__ . '/../../views/console/databases/index.phtml');
$layout
->setParam('title', APP_NAME . ' - Database')
->setParam('body', $page);
});
App::get('/console/database/collection')
App::get('/console/databases/database')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->param('id', '', new UID(), 'Database unique ID.')
->inject('response')
->inject('layout')
->action(function (string $id, Response $response, View $layout) {
$logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'database.listLogs')
->setParam('params', [
'database-id' => '{{router.params.id}}',
])
;
$page = new View(__DIR__ . '/../../views/console/databases/database.phtml');
$page->setParam('logs', $logs);
$layout
->setParam('title', APP_NAME . ' - Database')
->setParam('body', $page)
;
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Expires', 0)
->addHeader('Pragma', 'no-cache')
;
});
App::get('/console/databases/collection')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
@@ -242,13 +277,14 @@ App::get('/console/database/collection')
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'database.listCollectionLogs')
->setParam('method', 'databases.listCollectionLogs')
->setParam('params', [
'collection-id' => '{{router.params.id}}',
'database-id' => '{{router.params.databaseId}}'
])
;
$page = new View(__DIR__ . '/../../views/console/database/collection.phtml');
$page = new View(__DIR__ . '/../../views/console/databases/collection.phtml');
$page->setParam('logs', $logs);
@@ -264,29 +300,32 @@ App::get('/console/database/collection')
;
});
App::get('/console/database/document')
App::get('/console/databases/document')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->param('databaseId', '', new UID(), 'Database unique ID.')
->param('collection', '', new UID(), 'Collection unique ID.')
->inject('layout')
->action(function (string $collection, View $layout) {
->action(function (string $databaseId, string $collection, View $layout) {
$logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'database.listDocumentLogs')
->setParam('method', 'databases.listDocumentLogs')
->setParam('params', [
'database-id' => '{{router.params.databaseId}}',
'collection-id' => '{{router.params.collection}}',
'document-id' => '{{router.params.id}}',
])
;
$page = new View(__DIR__ . '/../../views/console/database/document.phtml');
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', false)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('logs', $logs)
;
@@ -296,18 +335,20 @@ App::get('/console/database/document')
->setParam('body', $page);
});
App::get('/console/database/document/new')
App::get('/console/databases/document/new')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->param('databaseId', '', new UID(), 'Database unique ID.')
->param('collection', '', new UID(), 'Collection unique ID.')
->inject('layout')
->action(function (string $collection, View $layout) {
->action(function (string $databaseId, string $collection, View $layout) {
$page = new View(__DIR__ . '/../../views/console/database/document.phtml');
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', true)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('logs', new View())
;
Binary file not shown.
Binary file not shown.
+7 -2
View File
@@ -118,6 +118,13 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
if (!$dbForConsole->getCollection($key)->isEmpty()) {
continue;
}
/**
* Skip to prevent 0.15 migration issues.
*/
if ($key === 'databases' && $dbForConsole->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'), 'collections')) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
$attributes = [];
@@ -155,8 +162,6 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
$dbForConsole->createDocument('buckets', new Document([
'$id' => 'default',
'$collection' => 'buckets',
'dateCreated' => \time(),
'dateUpdated' => \time(),
'name' => 'Default',
'permission' => 'file',
'maximumFileSize' => (int) App::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
+49 -16
View File
@@ -23,11 +23,17 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\DSN\DSN;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone;
use Appwrite\GraphQL\Builder;
use Appwrite\GraphQL\CoroutinePromiseAdapter;
use Appwrite\Network\Validator\Email;
@@ -81,9 +87,10 @@ const APP_LIMIT_ANTIVIRUS = 20000000; //20MB
const APP_LIMIT_ENCRYPTION = 20000000; //20MB
const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
const APP_CACHE_BUSTER = 305;
const APP_VERSION_STABLE = '0.14.2';
const APP_CACHE_BUSTER = 401;
const APP_VERSION_STABLE = '0.15.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -121,6 +128,7 @@ const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex';
const BUILD_TYPE_DEPLOYMENT = 'deployment';
const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_PROJECTS = 'projects';
@@ -135,6 +143,7 @@ const DELETE_TYPE_CERTIFICATES = 'certificates';
const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_SESSIONS = 'sessions';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@@ -259,8 +268,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getAttributeLimit());
new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]),
new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')])
], $database->getAttributeLimit(), 0, []);
}
);
@@ -272,7 +282,8 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('indexes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]),
new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')])
], 64);
}
);
@@ -285,7 +296,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
}
);
@@ -298,7 +309,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
}
);
@@ -311,7 +322,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
}
);
@@ -324,7 +335,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
}
);
@@ -336,7 +347,7 @@ Database::addFilter(
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database->find('sessions', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
}
);
@@ -349,7 +360,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('tokens', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
}
);
@@ -362,7 +373,7 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('memberships', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
}
);
@@ -463,6 +474,11 @@ $register->set('dbPool', function () {
->withPassword($dbPass)
->withOptions([
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => true,
PDO::ATTR_STRINGIFY_FETCHES => true,
]),
64
);
@@ -544,7 +560,7 @@ $register->set('smtp', function () {
return $mail;
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/db/DBIP/dbip-country-lite-2022-03.mmdb');
return new Reader(__DIR__ . '/db/DBIP/dbip-country-lite-2022-06.mmdb');
});
$register->set('db', function () {
// This is usually for our workers or CLI commands scope
@@ -555,11 +571,12 @@ $register->set('db', function () {
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => true,
PDO::ATTR_STRINGIFY_FETCHES => true,
));
return $pdo;
@@ -691,6 +708,7 @@ App::setResource('audits', fn() => new Audit());
App::setResource('mails', fn() => new Mail());
App::setResource('deletes', fn() => new Delete());
App::setResource('database', fn() => new EventDatabase());
App::setResource('messaging', fn() => new Phone());
App::setResource('usage', function ($register) {
return new Stats($register->get('statsd'));
}, ['register']);
@@ -841,6 +859,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('console', function () {
return new Document([
'$id' => 'console',
'$internalId' => 'console',
'name' => 'Appwrite',
'$collection' => 'projects',
'description' => 'Appwrite core engine',
@@ -870,12 +889,12 @@ App::setResource('console', function () {
]);
}, []);
App::setResource('dbForProject', function ($db, $cache, $project) {
App::setResource('dbForProject', function ($db, $cache, Document $project) {
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace("_{$project->getId()}");
$database->setNamespace("_{$project->getInternalId()}");
return $database;
}, ['db', 'cache', 'project']);
@@ -967,6 +986,20 @@ App::setResource('geodb', function ($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('phone', function () {
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
return match ($dsn->getHost()) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
default => null
};
});
App::setResource('promiseAdapter', function ($register) {
/** @var Utopia\Registry\Registry $register */
return $register->get('promiseAdapter');
+34 -22
View File
@@ -112,8 +112,9 @@ function getDatabase(Registry &$register, string $namespace)
if (!$database->exists($database->getDefaultDatabase(), 'realtime')) {
throw new Exception('Collection not ready');
}
break; // leave loop if successful
} catch (\Exception $e) {
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
@@ -138,24 +139,30 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument, $logError) {
try {
[$database, $returnDatabase] = getDatabase($register, '_console');
$document = new Document([
'$id' => $database->getId(),
'$collection' => 'realtime',
'$read' => [],
'$write' => [],
'container' => $containerId,
'timestamp' => time(),
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
} catch (\Throwable $th) {
call_user_func($logError, $th, "createWorkerDocument");
} finally {
call_user_func($returnDatabase);
}
go(function () use ($register, $containerId, &$statsDocument) {
$attempts = 0;
[$database, $returnDatabase] = getDatabase($register, '_console');
do {
try {
$attempts++;
$document = new Document([
'$id' => $database->getId(),
'$collection' => 'realtime',
'$read' => [],
'$write' => [],
'container' => $containerId,
'timestamp' => time(),
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
break;
} catch (\Throwable $th) {
Console::warning("Collection not ready. Retrying connection ({$attempts})...");
sleep(DATABASE_RECONNECT_SLEEP);
}
} while (true);
call_user_func($returnDatabase);
});
/**
@@ -297,7 +304,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
[$database, $returnDatabase] = getDatabase($register, "_{$projectId}");
[$consoleDatabase, $returnConsoleDatabase] = getDatabase($register, '_console');
$project = Authorization::skip(fn() => $consoleDatabase->getDocument('projects', $projectId));
[$database, $returnDatabase] = getDatabase($register, "_{$project->getInternalId()}");
$user = $database->getDocument('users', $userId);
@@ -306,6 +315,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']);
call_user_func($returnDatabase);
call_user_func($returnConsoleDatabase);
}
}
@@ -373,7 +383,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace("_{$project->getId()}");
$database->setNamespace("_{$project->getInternalId()}");
/*
* Project Check
@@ -480,7 +490,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace("_{$realtime->connections[$connection]['projectId']}");
$database->setNamespace("_console");
$project = Authorization::skip(fn() => $database->getDocument('projects', $realtime->connections[$connection]['projectId']));
$database->setNamespace("_{$project->getInternalId()}");
/*
* Abuse Check
+3 -3
View File
@@ -33,7 +33,7 @@ $cli
* 3. Ask user to backup important volumes, env vars, and SQL tables
* In th future we can try and automate this for smaller/medium size setups
* 4. Drop new docker-compose.yml setup (located inside the container, no network dependencies with appwrite.io) - DONE
* 5. Run docker-compose up -d - DONE
* 5. Run docker compose up -d - DONE
* 6. Run data migration
*/
$config = Config::getParam('variables');
@@ -214,9 +214,9 @@ $cli
}
}
Console::log("Running \"docker-compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes\"");
Console::log("Running \"docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("${env} docker-compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
$exit = Console::execute("${env} docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';
+10
View File
@@ -3,6 +3,7 @@
global $cli;
global $register;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Utopia\App;
@@ -93,6 +94,14 @@ $cli
->trigger();
}
function notifyDeleteExpiredSessions()
{
(new Delete())
->setType(DELETE_TYPE_SESSIONS)
->setTimestamp(time() - Auth::TOKEN_EXPIRATION_LOGIN_LONG)
->trigger();
}
function renewCertificates($dbForConsole)
{
$time = date('d-m-Y H:i:s', time());
@@ -136,6 +145,7 @@ $cli
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
notifyDeleteExpiredSessions();
renewCertificates($database);
}, $interval);
});
+4 -3
View File
@@ -28,9 +28,9 @@ $cli
Console::success('Starting Data Migration to version ' . $version);
$db = $register->get('db', true);
$cache = $register->get('cache', true);
$cache = new Cache(new RedisCache($cache));
$redis = $register->get('cache', true);
$redis->flushAll();
$cache = new Cache(new RedisCache($redis));
$projectDB = new Database(new MariaDB($db), $cache);
$projectDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
@@ -79,5 +79,6 @@ $cli
}
Swoole\Event::wait(); // Wait for Coroutines to finish
$redis->flushAll();
Console::success('Data Migration Completed');
});
+2 -2
View File
@@ -30,7 +30,7 @@ $cli
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
$message = ($git) ? Console::confirm('Please enter your commit message:') : '';
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', 'latest'])) {
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', 'latest'])) {
throw new Exception('Unknown version given');
}
@@ -196,7 +196,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setTwitter(APP_SOCIAL_TWITTER_HANDLE)
->setDiscord(APP_SOCIAL_DISCORD_CHANNEL, APP_SOCIAL_DISCORD)
->setDefaultHeaders([
'X-Appwrite-Response-Format' => '0.14.0',
'X-Appwrite-Response-Format' => '0.15.0',
]);
try {
+74 -84
View File
@@ -123,104 +123,94 @@ $cli
],
];
foreach (['swagger2', 'open-api3'] as $format) {
foreach ($platforms as $platform) {
$routes = [];
$models = [];
$services = [];
foreach ($platforms as $platform) {
$routes = [];
$models = [];
$services = [];
foreach ($appRoutes as $key => $method) {
foreach ($method as $route) {
/** @var \Utopia\Route $route */
$routeSecurity = $route->getLabel('sdk.auth', []);
$sdkPlatofrms = [];
foreach ($appRoutes as $key => $method) {
foreach ($method as $route) {
/** @var \Utopia\Route $route */
$routeSecurity = $route->getLabel('sdk.auth', []);
$sdkPlaforms = [];
foreach ($routeSecurity as $value) {
switch ($value) {
case APP_AUTH_TYPE_SESSION:
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
break;
case APP_AUTH_TYPE_KEY:
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_JWT:
$sdkPlatofrms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_ADMIN:
$sdkPlatofrms[] = APP_PLATFORM_CONSOLE;
break;
}
foreach ($routeSecurity as $value) {
switch ($value) {
case APP_AUTH_TYPE_SESSION:
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
break;
case APP_AUTH_TYPE_KEY:
$sdkPlaforms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_JWT:
$sdkPlaforms[] = APP_PLATFORM_SERVER;
break;
case APP_AUTH_TYPE_ADMIN:
$sdkPlaforms[] = APP_PLATFORM_CONSOLE;
break;
}
if (empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
if (!$route->getLabel('docs', true)) {
continue;
}
if ($route->getLabel('sdk.mock', false) && !$mocks) {
continue;
}
if (!$route->getLabel('sdk.mock', false) && $mocks) {
continue;
}
if (empty($route->getLabel('sdk.namespace', null))) {
continue;
}
if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlatofrms)) {
continue;
}
$routes[] = $route;
$modelLabel = $route->getLabel('sdk.response.model', 'none');
\is_array($modelLabel) ? \array_map(function ($m) use ($response) {
return $response->getModel($m);
}, $modelLabel) : $response->getModel($modelLabel);
}
}
foreach (Config::getParam('services', []) as $service) {
if (
!isset($service['docs']) // Skip service if not part of the public API
|| !isset($service['sdk'])
|| !$service['docs']
|| !$service['sdk']
) {
if (empty($routeSecurity)) {
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
}
if (!$route->getLabel('docs', true)) {
continue;
}
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
];
}
$models = $response->getModels();
foreach ($models as $key => $value) {
if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
if ($route->getLabel('sdk.mock', false) && !$mocks) {
continue;
}
if (!$route->getLabel('sdk.mock', false) && $mocks) {
continue;
}
if (empty($route->getLabel('sdk.namespace', null))) {
continue;
}
if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlaforms)) {
continue;
}
$routes[] = $route;
}
}
foreach (Config::getParam('services', []) as $service) {
if (
!isset($service['docs']) // Skip service if not part of the public API
|| !isset($service['sdk'])
|| !$service['docs']
|| !$service['sdk']
) {
continue;
}
switch ($format) {
case 'swagger2':
$formatInstance = new Swagger2(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
break;
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
'x-globalAttributes' => $service['globalAttributes'] ?? [],
];
}
case 'open-api3':
$formatInstance = new OpenAPI3(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
break;
$models = $response->getModels();
default:
throw new Exception('Format not found: ' . $format);
break;
foreach ($models as $key => $value) {
if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
}
}
// var_dump($models);
$arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0];
foreach (['swagger2', 'open-api3'] as $format) {
$formatInstance = match ($format) {
'swagger2' => new Swagger2(...$arguments),
'open-api3' => new OpenAPI3(...$arguments),
default => throw new Exception('Format not found: ' . $format)
};
$specs = new Specification($formatInstance);
$endpoint = App::getEnv('_APP_HOME', '[HOSTNAME]');
+110 -772
View File
@@ -2,271 +2,131 @@
global $cli, $register;
use Appwrite\Stats\Usage;
use Appwrite\Stats\UsageDB;
use InfluxDB\Database as InfluxDatabase;
use Utopia\App;
use Utopia\Cache\Adapter\Redis;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
use Utopia\Logger\Log;
/**
* Metrics We collect
*
* General
*
* requests
* network
* executions
*
* Database
*
* database.collections.create
* database.collections.read
* database.collections.update
* database.collections.delete
* database.documents.create
* database.documents.read
* database.documents.update
* database.documents.delete
* database.collections.{collectionId}.documents.create
* database.collections.{collectionId}.documents.read
* database.collections.{collectionId}.documents.update
* database.collections.{collectionId}.documents.delete
*
* Storage
*
* storage.buckets.create
* storage.buckets.read
* storage.buckets.update
* storage.buckets.delete
* storage.files.create
* storage.files.read
* storage.files.update
* storage.files.delete
* storage.buckets.{bucketId}.files.create
* storage.buckets.{bucketId}.files.read
* storage.buckets.{bucketId}.files.update
* storage.buckets.{bucketId}.files.delete
*
* Users
*
* users.create
* users.read
* users.update
* users.delete
* users.sessions.create
* users.sessions.{provider}.create
* users.sessions.delete
*
* Functions
*
* functions.{functionId}.executions
* functions.{functionId}.failures
* functions.{functionId}.compute
*
* Counters
*
* users.count
* storage.buckets.count
* storage.files.count
* storage.buckets.{bucketId}.files.count
* database.collections.count
* database.documents.count
* database.collections.{collectionId}.documents.count
*
* Totals
*
* storage.total
*
*/
Authorization::disable();
Authorization::setDefaultStatus(false);
function getDatabase(Registry &$register, string $namespace): Database
{
$attempts = 0;
do {
try {
$attempts++;
$db = $register->get('db');
$redis = $register->get('cache');
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
if (!$database->exists($database->getDefaultDatabase(), 'projects')) {
throw new Exception('Projects collection not ready');
}
break; // leave loop if successful
} catch (\Exception$e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return $database;
}
function getInfluxDB(Registry &$register): InfluxDatabase
{
/** @var InfluxDB\Client $client */
$client = $register->get('influxdb');
$attempts = 0;
$max = 10;
$sleep = 1;
do { // check if telegraf database is ready
try {
$attempts++;
$database = $client->selectDB('telegraf');
if (in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable$th) {
Console::warning("InfluxDB not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('InfluxDB database not ready yet');
}
sleep($sleep);
}
} while ($attempts < $max);
return $database;
}
$logError = function (Throwable $error, string $action = 'syncUsageStats') use ($register) {
$logger = $register->get('logger');
if ($logger) {
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
$log->setAction($action);
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
Console::info('Usage stats log pushed with status code: ' . $responseCode);
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
};
$cli
->task('usage')
->desc('Schedules syncing data from influxdb to Appwrite console db')
->action(function () use ($register) {
->action(function () use ($register, $logError) {
Console::title('Usage Aggregation V1');
Console::success(APP_NAME . ' usage aggregation process v1 has started');
$interval = (int) App::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '30'); // 30 seconds (by default)
$periods = [
[
'key' => '30m',
'startTime' => '-24 hours',
],
[
'key' => '1d',
'startTime' => '-90 days',
],
];
// all the metrics that we are collecting at the moment
$globalMetrics = [
'requests' => [
'table' => 'appwrite_usage_requests_all',
],
'network' => [
'table' => 'appwrite_usage_network_all',
],
'executions' => [
'table' => 'appwrite_usage_executions_all',
],
'database.collections.create' => [
'table' => 'appwrite_usage_database_collections_create',
],
'database.collections.read' => [
'table' => 'appwrite_usage_database_collections_read',
],
'database.collections.update' => [
'table' => 'appwrite_usage_database_collections_update',
],
'database.collections.delete' => [
'table' => 'appwrite_usage_database_collections_delete',
],
'database.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
],
'database.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
],
'database.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
],
'database.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
],
'database.collections.collectionId.documents.create' => [
'table' => 'appwrite_usage_database_documents_create',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.read' => [
'table' => 'appwrite_usage_database_documents_read',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.update' => [
'table' => 'appwrite_usage_database_documents_update',
'groupBy' => 'collectionId',
],
'database.collections.collectionId.documents.delete' => [
'table' => 'appwrite_usage_database_documents_delete',
'groupBy' => 'collectionId',
],
'storage.buckets.create' => [
'table' => 'appwrite_usage_storage_buckets_create',
],
'storage.buckets.read' => [
'table' => 'appwrite_usage_storage_buckets_read',
],
'storage.buckets.update' => [
'table' => 'appwrite_usage_storage_buckets_update',
],
'storage.buckets.delete' => [
'table' => 'appwrite_usage_storage_buckets_delete',
],
'storage.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
],
'storage.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
],
'storage.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
],
'storage.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
],
'storage.buckets.bucketId.files.create' => [
'table' => 'appwrite_usage_storage_files_create',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.read' => [
'table' => 'appwrite_usage_storage_files_read',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.update' => [
'table' => 'appwrite_usage_storage_files_update',
'groupBy' => 'bucketId',
],
'storage.buckets.bucketId.files.delete' => [
'table' => 'appwrite_usage_storage_files_delete',
'groupBy' => 'bucketId',
],
'users.create' => [
'table' => 'appwrite_usage_users_create',
],
'users.read' => [
'table' => 'appwrite_usage_users_read',
],
'users.update' => [
'table' => 'appwrite_usage_users_update',
],
'users.delete' => [
'table' => 'appwrite_usage_users_delete',
],
'users.sessions.create' => [
'table' => 'appwrite_usage_users_sessions_create',
],
'users.sessions.provider.create' => [
'table' => 'appwrite_usage_users_sessions_create',
'groupBy' => 'provider',
],
'users.sessions.delete' => [
'table' => 'appwrite_usage_users_sessions_delete',
],
'functions.functionId.executions' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
],
'functions.functionId.compute' => [
'table' => 'appwrite_usage_executions_time',
'groupBy' => 'functionId',
],
'functions.functionId.failures' => [
'table' => 'appwrite_usage_executions_all',
'groupBy' => 'functionId',
'filters' => [
'functionStatus' => 'failed',
],
],
];
$database = getDatabase($register, '_console');
$influxDB = getInfluxDB($register);
// TODO Maybe move this to the setResource method, and reuse in the http.php file
$attempts = 0;
$max = 10;
$sleep = 1;
$db = null;
$redis = null;
do { // connect to db
try {
$attempts++;
$db = $register->get('db');
$redis = $register->get('cache');
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
// TODO use inject
$cacheAdapter = new Cache(new Redis($redis));
$dbForProject = new Database(new MariaDB($db), $cacheAdapter);
$dbForConsole = new Database(new MariaDB($db), $cacheAdapter);
$dbForProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForConsole->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$dbForConsole->setNamespace('_console');
$latestTime = [];
Authorization::disable();
$usage = new Usage($database, $influxDB, $logError);
$usageDB = new UsageDB($database, $logError);
$iterations = 0;
Console::loop(function () use ($interval, $register, $dbForProject, $dbForConsole, $globalMetrics, $periods, &$latestTime, &$iterations) {
Console::loop(function () use ($interval, $usage, $usageDB, &$iterations) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating usage data every {$interval} seconds");
@@ -274,105 +134,10 @@ $cli
/**
* Aggregate InfluxDB every 30 seconds
* @var InfluxDB\Client $client
*/
$client = $register->get('influxdb');
$attempts = 0;
$max = 10;
$sleep = 1;
$usage->collect();
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);
// sync data
foreach ($globalMetrics as $metric => $options) { //for each metrics
foreach ($periods as $period) { // aggregate data for each period
$start = DateTime::createFromFormat('U', \strtotime($period['startTime']))->format(DateTime::RFC3339);
if (!empty($latestTime[$metric][$period['key']])) {
$start = DateTime::createFromFormat('U', $latestTime[$metric][$period['key']])->format(DateTime::RFC3339);
}
$end = DateTime::createFromFormat('U', \strtotime('now'))->format(DateTime::RFC3339);
$table = $options['table']; //Which influxdb table to query for this metric
$groupBy = empty($options['groupBy']) ? '' : ', "' . $options['groupBy'] . '"'; //Some sub level metrics may be grouped by other tags like collectionId, bucketId, etc
$filters = $options['filters'] ?? []; // Some metrics might have additional filters, like function's status
if (!empty($filters)) {
$filters = ' AND ' . implode(' AND ', array_map(fn ($filter, $value) => "\"{$filter}\"='{$value}'", array_keys($filters), array_values($filters)));
} else {
$filters = '';
}
$query = "SELECT sum(value) AS \"value\" FROM \"{$table}\" WHERE \"time\" > '{$start}' AND \"time\" < '{$end}' AND \"metric_type\"='counter' {$filters} GROUP BY time({$period['key']}), \"projectId\" {$groupBy} FILL(null)";
try {
$result = $database->query($query);
$points = $result->getPoints();
foreach ($points as $point) {
$projectId = $point['projectId'];
if (!empty($projectId) && $projectId !== 'console') {
$dbForProject->setNamespace('_' . $projectId);
$metricUpdated = $metric;
if (!empty($groupBy)) {
$groupedBy = $point[$options['groupBy']] ?? '';
if (empty($groupedBy)) {
continue;
}
$metricUpdated = str_replace($options['groupBy'], $groupedBy, $metric);
}
$time = \strtotime($point['time']);
$id = \md5($time . '_' . $period['key'] . '_' . $metricUpdated); //Construct unique id for each metric using time, period and metric
$value = (!empty($point['value'])) ? $point['value'] : 0;
try {
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => $period['key'],
'time' => $time,
'metric' => $metricUpdated,
'value' => $value,
'type' => 0,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
$latestTime[$metric][$period['key']] = $time;
} catch (\Exception $e) { // if projects are deleted this might fail
Console::warning("Failed to save data for project {$projectId} and metric {$metricUpdated}: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
} catch (\Exception $e) {
Console::warning("Failed to Query: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
if ($iterations % 30 != 0) { // Aggregate aggregate number of objects in database only after 15 minutes
if ($iterations % 30 != 0) { // return if 30 iterations has not passed
$iterations++;
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
@@ -380,6 +145,7 @@ $cli
return;
}
$iterations = 0; // Reset iterations to prevent overflow when running for long time
/**
* Aggregate MariaDB every 15 minutes
* Some of the queries here might contain full-table scans.
@@ -387,435 +153,7 @@ $cli
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Aggregating database counters.");
$latestProject = null;
do { // Loop over all the projects
$attempts = 0;
$max = 10;
$sleep = 1;
do { // list projects
try {
$attempts++;
$projects = $dbForConsole->find('projects', [], 100, cursor: $latestProject);
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Console DB not ready yet. Retrying ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed access console db: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
if (empty($projects)) {
continue;
}
$latestProject = $projects[array_key_last($projects)];
foreach ($projects as $project) {
$projectId = $project->getId();
// Get total storage
$dbForProject->setNamespace('_' . $projectId);
$deploymentsTotal = $dbForProject->sum('deployments', 'size');
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_storage.deployments.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
try {
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '30m',
'time' => $time,
'metric' => 'storage.deployments.total',
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $deploymentsTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_storage.deployments.total'); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => '1d',
'time' => $time,
'metric' => 'storage.deployments.total',
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $deploymentsTotal)
);
}
} catch (\Exception $e) {
Console::warning("Failed to save data for project {$projectId} and metric storage.deployments.total: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
$collections = [
'users' => [
'namespace' => '',
],
'collections' => [
'metricPrefix' => 'database',
'namespace' => '',
'subCollections' => [ // Some collections, like collections and later buckets have child collections that need counting
'documents' => [
'collectionPrefix' => 'collection_',
'namespace' => '',
],
],
],
'buckets' => [
'metricPrefix' => 'storage',
'namespace' => '',
'subCollections' => [
'files' => [
'namespace' => '',
'collectionPrefix' => 'bucket_',
'total' => [
'field' => 'sizeOriginal'
]
],
]
]
];
foreach ($collections as $collection => $options) {
try {
$dbForProject->setNamespace("_{$projectId}");
$count = $dbForProject->count($collection);
$metricPrefix = $options['metricPrefix'] ?? '';
$metric = empty($metricPrefix) ? "{$collection}.count" : "{$metricPrefix}.{$collection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$subCollections = $options['subCollections'] ?? [];
if (empty($subCollections)) {
continue;
}
$latestParent = null;
$subCollectionCounts = []; //total project level count of sub collections
$subCollectionTotals = []; //total project level sum of sub collections
do { // Loop over all the parent collection document for each sub collection
$dbForProject->setNamespace("_{$projectId}");
$parents = $dbForProject->find($collection, [], 100, cursor: $latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
if (empty($parents)) {
continue;
}
$latestParent = $parents[array_key_last($parents)];
foreach ($parents as $parent) {
foreach ($subCollections as $subCollection => $subOptions) { // Sub collection counts, like database.collections.collectionId.documents.count
$dbForProject->setNamespace("_{$projectId}");
$count = $dbForProject->count(($subOptions['collectionPrefix'] ?? '') . $parent->getInternalId());
$subCollectionCounts[$subCollection] = ($subCollectionCounts[$subCollection] ?? 0) + $count; // Project level counts for sub collections like database.documents.count
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$collection}.{$parent->getId()}.{$subCollection}.count" : "{$metricPrefix}.{$collection}.{$parent->getInternalId()}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
// check if sum calculation is required
$total = $subOptions['total'] ?? [];
if (empty($total)) {
continue;
}
$dbForProject->setNamespace("_{$projectId}");
$total = (int) $dbForProject->sum(($subOptions['collectionPrefix'] ?? '') . $parent->getInternalId(), $total['field']);
$subCollectionTotals[$subCollection] = ($ssubCollectionTotals[$subCollection] ?? 0) + $total; // Project level sum for sub collections like storage.total
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$collection}.{$parent->getId()}.{$subCollection}.total" : "{$metricPrefix}.{$collection}.{$parent->getInternalId()}.{$subCollection}.total";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $total,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $total,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
}
}
} while (!empty($parents));
/**
* Inserting project level counts for sub collections like database.documents.count
*/
foreach ($subCollectionCounts as $subCollection => $count) {
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$subCollection}.count" : "{$metricPrefix}.{$subCollection}.count";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
}
/**
* Inserting project level sums for sub collections like storage.files.total
*/
foreach ($subCollectionTotals as $subCollection => $count) {
$dbForProject->setNamespace("_{$projectId}");
$metric = empty($metricPrefix) ? "{$subCollection}.total" : "{$metricPrefix}.{$subCollection}.total";
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
// aggregate storage.total = storage.files.total + storage.deployments.total
if ($metricPrefix === 'storage' && $subCollection === 'files') {
$metric = 'storage.total';
$time = (int) (floor(time() / 1800) * 1800); // Time rounded to nearest 30 minutes
$id = \md5($time . '_30m_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '30m',
'metric' => $metric,
'value' => $count + $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count + $deploymentsTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
$id = \md5($time . '_1d_' . $metric); //Construct unique id for each metric using time, period and metric
$document = $dbForProject->getDocument('stats', $id);
if ($document->isEmpty()) {
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'time' => $time,
'period' => '1d',
'metric' => $metric,
'value' => $count + $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count + $deploymentsTotal)
);
}
}
}
} catch (\Exception$e) {
Console::warning("Failed to save database counters data for project {$collection}: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
}
}
} while (!empty($projects));
$usageDB->collect();
$iterations++;
$loopTook = microtime(true) - $loopStart;
+1 -1
View File
@@ -97,7 +97,7 @@
<ul class="links">
<li>
<a data-ls-attrs="href=/console/database?project={{router.params.project}}"
<a data-ls-attrs="href=/console/databases?project={{router.params.project}}"
data-analytics
data-analytics-event="click"
data-analytics-category="console/navigation"
@@ -1,57 +0,0 @@
<?php
$key = $this->getParam('key', '');
$type = $this->getParam('type', '');
$required = $this->getParam('required', '');
$comp = $this->getParam('comp', '');
$namespace = $this->getParam('namespace', '');
$list = $this->getParam('list', []);
$collections = $this->getParam('collections', []);
$comp->setParam('namespace', 'node');
if($type === 'document') {
$comp->setParam('key', null);
}
?>
<input type="hidden" name="<?php echo $this->escape($key); ?>"<?php if($required): ?> required<?php endif; ?> data-cast-to="array-empty">
<hr />
<ul data-ls-loop="<?php echo $this->escape($namespace); ?>" data-ls-as="node" class="sortable numbers">
<li data-forms-move-up data-forms-move-down>
<div class="drop-list bottom end settings" data-ls-ui-open="" data-button-text="" data-button-icon="icon-cog" data-button-aria="Options" data-button-selector="[data-toggler]" data-button-class="round dark small margin-bottom-small margin-top-tiny pull-end" data-blur="1">
<ul class="arrow-end margin-top margin-end-negative-small">
<li data-move-up>
<button type="button" class="link"><i class="icon-up-dir"></i> Move Up</button>
</li>
<li data-move-down>
<button type="button" class="link"><i class="icon-down-dir"></i> Move Down</button>
</li>
<li>
<button type="button" data-ls-ui-trigger="splice-<?php echo $this->escape($namespace); ?>-{{$index}}" class="link"><i class="icon-cancel"></i> Remove</button>
</li>
</ul>
</div>
<?php echo $comp->render(); ?>
<hr />
</li>
</ul>
<?php if(!empty($list) && $type === 'document'): ?>
<div class="drop-list" data-ls-ui-open="" data-button-text="Add" data-button-aria="Add" data-button-icon="" data-button-selector="[data-toggler]" data-button-class="reverse margin-bottom-small" data-blur="1">
<ul>
<?php foreach($list as $item):
$name = (isset($collections[$item])) ? $collections[$item]->getAttribute('name', '') : '';
?>
<li>
<button type="button" class="link" data-ls-ui-trigger="collection-child-<?php echo $this->escape($namespace); ?>-<?php echo $this->escape($item); ?>"><i class="icon-edit"></i> Add <?php echo $this->escape($name); ?></button>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<button type="button" data-ls-ui-trigger="collection-child-<?php echo $this->escape($namespace); ?>" class="reverse margin-bottom-small">Add</button>
<?php endif; ?>
@@ -1,7 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="hidden" data-forms-switch data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" data-cast-to="boolean"<?php if($required): ?> required<?php endif; ?> class="margin-bottom-no" />
@@ -1,19 +0,0 @@
<?php
$namespace = $this->getParam('namespace', '');
$array = $this->getParam('array', false);
$list = $this->getParam('list', []);
$list = (is_array($list)) ? $list : [];
$list = ($array) ? $list : array_shift($list);
?>
<?php if($array): ?>
<div data-ls-template="collection-array-{{<?php echo $this->escape($namespace); ?>.$collection}}" data-type="script" class="margin-bottom-negative-small"></div>
<?php else: ?>
<hr class="margin-top-no" />
<div class="clear">
<a data-ls-if="(!{{<?php echo $this->escape($namespace); ?>.$id}})" data-ls-attrs="href=/console/database/document?&collection=<?php echo $this->escape($list); ?>&project={{router.params.project}}" class="pull-end text-size-small">New Document <i class="icon-link-ext"></i></a>
<a data-ls-if="({{<?php echo $this->escape($namespace); ?>.$id}})" data-ls-attrs="href=/console/database/document?id={{<?php echo $this->escape($namespace); ?>.$id}}&collection=<?php echo $this->escape($list); ?>&project={{router.params.project}}" class="pull-end text-size-small"><span data-ls-bind="Document #{{<?php echo $this->escape($namespace); ?>.$id}}"></span> <i class="icon-link-ext"></i></a>
</div>
<div data-ls-template="collection-<?php echo $this->escape($list); ?>" data-type="script" class="margin-bottom-negative-small"></div>
<hr />
<?php endif; ?>
@@ -1,57 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
$collections = $this->getParam('collections', []);
$array = $this->getParam('array', false);
$list = $this->getParam('list', []);
$list = (is_array($list)) ? $list : [];
?>
<?php foreach($list as $item):
$collection = $collections[$item] ?? null;
if(empty($collection)) {
continue;
}
$rules = $collection->getAttribute('rules', []);
?>
<div data-ls-if="({{<?php echo $this->escape($namespace); ?>}})"
data-service="database.getDocument"
data-param-collection-id="<?php echo $this->escape($collection->getId()); ?>"
data-param-document-id="{{<?php echo $this->escape($namespace); ?>}}"
data-scope="sdk"
data-event="load,document-selected-<?php echo $this->escape($collection->getId()); ?>"
data-name="project-document-preview"
data-success="default">
<div class="box line margin-bottom-small padding-small fade-bottom">
<ul>
<?php foreach($rules as $i => $rule):
$collectionLabel = $rule['label'] ?? '';
$collectionKey = $rule['key'] ?? '';
if($i === 3) {break;}
?>
<li class="margin-bottom-small">
<p><b><?php echo $this->escape($collectionLabel); ?>: </b> <span data-ls-bind="{{project-document-preview.<?php echo $this->escape($collectionKey); ?>|limit}}" data-unsync="1"></span></p>
</li>
<?php endforeach; ?>
</ul>
<span class="label tag blue">preview</span>
</div>
</div>
<input
type="text" data-forms-document-preview="<?php echo $this->escape($collection->getId()); ?>"
name="<?php echo $this->escape($key); ?>"
data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}"
class="margin-bottom" disabled<?php if($required): ?> required<?php endif; ?>>
<button class="reverse margin-end-small" type="button"
data-forms-document="open-document-serach-<?php echo $this->escape($collection->getId()); ?>"
data-search="<?php echo $this->escape($namespace); ?>"><i class="icon-search"></i> <?php echo $this->escape($collection->getAttribute('name')); ?></button>
<?php endforeach; ?>
@@ -1,6 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="email" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" placeholder="me@example.com"<?php if($required): ?> required<?php endif; ?> class="margin-bottom-no">
@@ -1,20 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input
type="hidden"
name="<?php echo $this->escape($key); ?>"
data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}"
data-read="<?php echo $this->escape(json_encode([])); ?>"
data-write="<?php echo $this->escape(json_encode([])); ?>"
data-accept=""
data-forms-upload=""
data-label-button="Upload"
data-search="<?php echo $this->escape($namespace); ?>"
data-scope="sdk"
data-default=""
data-project="{{router.params.project}}"
<?php if($required): ?> required<?php endif; ?>
class="margin-bottom-no">
@@ -1,6 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="text" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" minlength="7" maxlength="15" size="15" pattern="^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$" title="Please enter a valid IPV4 address" placeholder="255.255.255.255"<?php if($required): ?> required<?php endif; ?> class="margin-bottom-no">
@@ -1,7 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<textarea type="text" name="<?php echo $this->escape($key); ?>" data-forms-pell data-forms-text-direction data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}"<?php if($required): ?> required<?php endif; ?>></textarea>
@@ -1,7 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="number" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" data-cast-to="numeric"<?php if($required): ?> required<?php endif; ?> class="margin-bottom-no" step=any />
@@ -1,6 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="text" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" data-forms-text-direction data-forms-text-count<?php if($required): ?> required<?php endif; ?> />
@@ -1,6 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<input name="<?php echo $this->escape($key); ?>" type="url" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" placeholder="https://example.com"<?php if($required): ?> required<?php endif; ?> />
@@ -1,6 +0,0 @@
<?php
$key = $this->getParam('key', '');
$required = $this->getParam('required', '');
$namespace = $this->getParam('namespace', '');
?>
<textarea name="<?php echo $this->escape($key); ?>" type="text" autocomplete="off" data-ls-bind="{{<?php echo $this->escape($namespace); ?>}}" data-forms-text-direction data-forms-text-count<?php if($required): ?> required<?php endif; ?>></textarea>
@@ -1,148 +0,0 @@
<?php
$collection = $this->getParam('collection', []);
$id = $collection->getId();
$name = $collection->getAttribute('name', 'Collection');
$rules = $collection->getAttribute('rules', []);
?>
<div data-ui-modal class="modal sticky-footer width-large box close" data-button-hide="on" data-open-event="open-document-serach-<?php echo $this->escape($id); ?>" data-close-event="none">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2><?php echo $this->escape($name); ?> Search</h2>
<form class="search margin-bottom"
data-service="database.listDocuments"
data-event="submit"
data-param-collection-id="<?php echo $this->escape($id); ?>"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="0"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-documents"
data-success="state"
data-success-param-state-keys="search,offset">
<input name="search" id="searchDocuments-<?php echo $this->escape($id); ?>" type="search" autocomplete="off" placeholder="Search" class="margin-bottom-no" data-ls-bind="{{router.params.search}}">
</form>
<div
data-service="database.listDocuments"
data-event="open-document-serach-<?php echo $this->escape($id); ?>"
data-param-collection-id="<?php echo $this->escape($id); ?>"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="0"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-documents">
<div data-ls-if="0 == {{project-documents.total}}" class="margin-bottom">
<h3 class="margin-bottom-small text-bold">No Documents Found</h3>
<p class="margin-bottom-no">Try a different search term.</p>
</div>
<div data-ls-if="({{project-documents.total}})">
<form class="scroll">
<table class="margin-top-no margin-bottom-no">
<thead>
<tr>
<th width="40">&nbsp;</th>
<?php foreach($rules as $rule):
$label = $rule['label'] ?? '';
?>
<th width="120"><?php echo $this->escape($label); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody data-ls-loop="project-documents.documents" data-ls-as="node">
<tr>
<td data-title="x" class="">
<!-- <input type="radio" name="selected" data-ls-attrs="value={{file.$id}}" data-ls-bind="{{search.selected}}" /> -->
<input type="radio" name="selected" data-ls-attrs="value={{node.$id}}" data-ls-bind="{{search.selected}}" />
</td>
<?php foreach($rules as $rule):
$label = $rule['label'] ?? '';
$key = $rule['key'] ?? '';
$type = $rule['type'] ?? '';
$array = $rule['array'] ?? '';
?>
<td data-title="<?php echo $this->escape($label); ?>" class="text-size-small text-height-small">
<a data-ls-attrs="href=/console/database/document?id={{node.$id}}&collection=<?php echo $this->escape($id); ?>&project={{router.params.project}}" target="_blank">
<?php if(!$array): ?>
<?php switch($type):
case 'fileId': ?>
<img data-ls-if="{{node.<?php echo $this->escape($key); ?>}} != ''" src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{node.<?php echo $this->escape($key); ?>}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="avatar" width="30" height="30" loading="lazy" />
<?php break; ?>
<?php case 'document': ?>
{...}
<?php break; ?>
<?php default: ?>
<span data-ls-bind="{{node.<?php echo $this->escape($key); ?>}}" data-ls-attrs="title={{node.<?php echo $this->escape($key); ?>}}"></span>
<?php break; ?>
<?php endswitch; ?>
<?php else: ?>
[...]
<?php endif; ?>
</a>
</td>
<?php endforeach; ?>
</tr>
</tbody>
</table>
</form>
</div>
</div>
<footer>
<div class="clear text-align-center paging pull-end">
<form
data-service="database.listDocuments"
data-event="submit"
data-param-collection-id="<?php echo $this->escape($id); ?>"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-documents"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-documents.total|pageTotal}}"></span>
<form
data-service="database.listDocuments"
data-event="submit"
data-param-collection-id="<?php echo $this->escape($id); ?>"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-documents"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<form data-service="container.path"
data-param-path="search.selected"
data-param-type="assign"
data-param-value="{{node.$id}}"
data-success="trigger"
data-success-param-trigger-events="modal-close,document-selected-<?php echo $this->escape($id); ?>"
data-event="click"
data-scope="window.ls">
<input type="hidden" name="path" data-ls-bind="{{search.path}}" />
<input type="hidden" name="type" value="assign" />
<input type="hidden" name="value" data-ls-bind="{{search.selected}}" />
<button data-ls-if="({{search.selected}})" type="button" class="">Select</button> &nbsp;
<button data-ls-if="(!{{search.selected}})" type="button" class="" disabled>Select</button> &nbsp;
</form>
<button data-ui-modal-close="" type="button" class="reverse desktops-only-inline">Cancel</button>
</footer>
</div>
@@ -1,131 +0,0 @@
<div data-ui-modal class="modal sticky-footer width-large box close" data-button-hide="on" data-open-event="open-file-serach" data-close-event="none">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>File Search</h2>
<form class="search margin-bottom"
data-service="storage.listFiles"
data-event="submit"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<input name="search" id="searchFiles" type="search" autocomplete="off" placeholder="Search" class="margin-bottom-no" data-ls-bind="{{router.params.search}}">
</form>
<div
data-service="storage.listFiles"
data-event="open-file-serach"
data-param-search=""
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="0"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<div data-ls-if="0 == {{project-files.total}}" class="margin-bottom">
<h3 class="margin-bottom-small text-bold">No Files Found</h3>
<p class="margin-bottom-no">Try a different search term.</p>
</div>
<div data-ls-if="0 != {{project-files.total}}">
<div class="scroll">
<table class="margin-top-no margin-bottom-no">
<thead>
<tr>
<th width="40">&nbsp;</th>
<th width="40">&nbsp;</th>
<th width="100">Filename</th>
<th width="100">Type</th>
<th width="100">Size</th>
<th width="80">Created</th>
<!-- <th width="30"></th> -->
</tr>
</thead>
<tbody data-ls-loop="project-files.files" data-ls-as="file">
<tr>
<td data-title="x" class="">
<input type="radio" name="selected" data-ls-attrs="value={{file.$id}}" data-ls-bind="{{search.selected}}" />
</td>
<td data-title="x" class="">
<img src="" data-ls-attrs="src={{env.ENDPOINT}}/v1/storage/files/{{file.$id}}/preview?width=65&height=65&project={{router.params.project}}&mode=admin" class="pull-start avatar" width="30" height="30" loading="lazy" />
</td>
<td data-title="Name: " class="text-one-liner">
<span data-ls-bind="{{file.name}}" data-ls-attrs="title={{file.name}}" class="text-fade text-size-small"></span>
</td>
<td data-title="Type: ">
<span data-ls-bind="{{file.mimeType}}" class="text-fade text-size-small"></span>
</td>
<td data-title="Size: ">
<span class="text-fade text-size-small" data-ls-bind="{{file.sizeOriginal|humanFileSize}}"></span>
<span class="text-fade text-size-small" data-ls-bind="{{file.sizeOriginal|humanFileUnit}}"></span>
</td>
<td data-title="Created: ">
<span class="text-fade text-size-small" data-ls-bind="{{file.dateCreated|dateText}}"></span>
</td>
<!-- <td class="hide">
<a target="_blank" data-ls-attrs="href="><i class="icon-link-ext"></i></a>
</td> -->
</tr>
</tbody>
</table>
</div>
</div>
</div>
<footer>
<div class="clear text-align-center paging pull-end">
<form
data-service="storage.listFiles"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-end-small round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-files.total|pageTotal}}"></span>
<form
data-service="storage.listFiles"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-start-small round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<form data-service="container.path"
data-param-path="search.selected"
data-param-type="assign"
data-param-value="{{file.$id}}"
data-success="trigger"
data-success-param-trigger-events="modal-close"
data-event="click"
data-scope="window.ls">
<input type="hidden" name="path" data-ls-bind="{{search.path}}" />
<input type="hidden" name="type" value="assign" />
<input type="hidden" name="value" data-ls-bind="{{search.selected}}" />
<button data-ls-if="({{search.selected}})" type="button" class="">Select</button> &nbsp;
<button data-ls-if="(!{{search.selected}})" type="button" class="" disabled>Select</button> &nbsp;
</form>
<button data-ui-modal-close="" type="button" class="reverse desktops-only-inline">Cancel</button>
</footer>
</div>
@@ -4,20 +4,28 @@ $logs = $this->getParam('logs', null);
?>
<div
data-service="database.getCollection"
data-service="databases.getCollection"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-scope="sdk"
data-event="load,database.updateCollection,database.createAttribute,database.deleteAttribute,database.createIndex,database.deleteIndex"
data-event="load,databases.updateCollection,databases.createAttribute,databases.deleteAttribute,databases.createIndex,databases.deleteIndex"
data-name="project-collection">
<div class="cover">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/database?project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> Database</a>
<div
data-service="databases.get"
data-param-database-id="{{router.params.databaseId}}"
data-scope="sdk"
data-event="load,databases.update"
data-name="project-database">
<div class="cover">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/databases/database?id={{router.params.databaseId}}&project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> <span data-ls-bind="{{project-database.name}}">&nbsp;&nbsp;</span></a>
<br />
<br />
<span data-ls-bind="{{project-collection.name}}">&nbsp;&nbsp;</span>
</h1>
<span data-ls-bind="{{project-collection.name}}">&nbsp;&nbsp;</span>
</h1>
</div>
</div>
<div data-ui-modal class="modal width-large box close" data-button-hide="on" data-open-event="open-json">
@@ -34,14 +42,15 @@ $logs = $this->getParam('logs', null);
<div class="zone xl">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/database/collection?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2>Documents</h2>
<div
data-service="database.listDocuments"
data-event="load,database.createDocument,database.updateDocument,database.deleteDocument"
data-service="databases.listDocuments"
data-event="load,databases.createDocument,databases.updateDocument,databases.deleteDocument"
data-param-collection-id="{{router.params.id}}"
data-param-database-id="{{router.params.databaseId}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
data-param-order-types-cast-to="array"
@@ -75,15 +84,15 @@ $logs = $this->getParam('logs', null);
</template>
</tr>
</thead>
<tbody data-ls-attrs="x-init=documents = {{project-documents.documents}}" x-data="{documents: []}">
<tbody data-ls-attrs="x-init=documents = {{project-documents.documents}}; databaseId='{{router.params.databaseId}}'" x-data="{documents: []}">
<template x-for="doc in documents">
<tr>
<td data-title="$id: ">
<a :href="`/console/database/document?id=${doc.$id}&collection=${doc.$collection}&project=${project}`" x-text="doc.$id"></a>
<a :href="`/console/databases/document?id=${doc.$id}&collection=${doc.$collection}&databaseId=${databaseId}&project=${project}`" x-text="doc.$id"></a>
</td>
<template x-for="attr in attributes">
<td x-show="attr.status === 'available'" :data-title="attr.key + ':'">
<a :href="`/console/database/document?id=${doc.$id}&collection=${doc.$collection}&project=${project}`">
<a :href="`/console/databases/document?id=${doc.$id}&collection=${doc.$collection}&databaseId=${databaseId}&project=${project}`">
<span x-text="doc[attr.key] ?? 'n/a'"></span>
</a>
</td>
@@ -98,8 +107,9 @@ $logs = $this->getParam('logs', null);
<div class="pull-end text-align-center paging">
<form
data-service="database.listDocuments"
data-service="databases.listDocuments"
data-event="submit"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
@@ -114,8 +124,9 @@ $logs = $this->getParam('logs', null);
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-documents.total|pageTotal}}"></span>
<form
data-service="database.listDocuments"
data-service="databases.listDocuments"
data-event="submit"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
@@ -128,15 +139,15 @@ $logs = $this->getParam('logs', null);
</form>
</div>
<a data-ls-if="0 < {{project-collection.attributes.length}}" data-ls-attrs="href=/console/database/document/new?collection={{router.params.id}}&project={{router.params.project}}" class="button">
<a data-ls-if="0 < {{project-collection.attributes.length}}" data-ls-attrs="href=/console/databases/document/new?collection={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}" class="button">
Add Document
</a>
<a data-ls-if="!{{project-collection.attributes.length}}" data-ls-attrs="href=/console/database/collection/attributes?id={{router.params.id}}&project={{router.params.project}}" class="button">
<a data-ls-if="!{{project-collection.attributes.length}}" data-ls-attrs="href=/console/databases/collection/attributes?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}" class="button">
Create Attribute
</a>
</div>
</li>
<li data-state="/console/database/collection/attributes?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection/attributes?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2>Attributes</h2>
<div class="clear box margin-bottom">
@@ -266,13 +277,14 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Collection Attribute"
data-service="database.deleteAttribute"
data-service="databases.deleteAttribute"
data-scope="sdk"
data-event="submit"
data-confirm="Are you sure you want to delete this attribute?"
data-param-database-id="{{router.params.databaseId}}"
data-success="alert,trigger"
data-success-param-alert-text="Deleted attribute successfully"
data-success-param-trigger-events="database.deleteAttribute"
data-success-param-trigger-events="databases.deleteAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to delete attribute"
data-failure-param-alert-classname="error">
@@ -320,7 +332,7 @@ $logs = $this->getParam('logs', null);
</ul>
</div>
</li>
<li data-state="/console/database/collection/indexes?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection/indexes?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2>Indexes</h2>
<div class="clear box margin-bottom">
@@ -382,13 +394,14 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Collection Index"
data-service="database.deleteIndex"
data-service="databases.deleteIndex"
data-scope="sdk"
data-event="submit"
data-confirm="Are you sure you want to delete this index?"
data-param-database-id="{{router.params.databaseId}}"
data-success="alert,trigger"
data-success-param-alert-text="Deleted index successfully"
data-success-param-trigger-events="database.deleteIndex"
data-success-param-trigger-events="databases.deleteIndex"
data-failure="alert"
data-failure-param-alert-text="Failed to delete index"
data-failure-param-alert-classname="error">
@@ -407,18 +420,19 @@ $logs = $this->getParam('logs', null);
<button class="new-index">Add Index</button>
</li>
<li data-state="/console/database/collection/activity?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection/activity?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2>Activity</h2>
<?php echo $logs->render(); ?>
</li>
<li data-state="/console/database/collection/usage?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection/usage?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="database.getCollectionUsage"
data-service="databases.getCollectionUsage"
data-event="submit"
data-name="usage"
data-param-collection-id="{{router.params.id}}"
data-param-database-id="{{router.params.databaseId}}"
data-param-range="90d">
<button class="tick">90d</button>
</form>
@@ -427,9 +441,10 @@ $logs = $this->getParam('logs', null);
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="database.getCollectionUsage"
data-service="databases.getCollectionUsage"
data-event="submit"
data-name="usage"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}">
<button class="tick">30d</button>
</form>
@@ -438,9 +453,10 @@ $logs = $this->getParam('logs', null);
<form
class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="database.getCollectionUsage"
data-service="databases.getCollectionUsage"
data-event="submit"
data-name="usage"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-param-range="24h">
<button class="tick">24h</button>
@@ -450,7 +466,7 @@ $logs = $this->getParam('logs', null);
<h2>Usage</h2>
<div data-service="database.getCollectionUsage" data-event="load" data-name="usage" data-param-collection-id="{{router.params.id}}">
<div data-service="databases.getCollectionUsage" data-event="load" data-name="usage" data-param-collection-id="{{router.params.id}}" data-param-database-id="{{router.params.databaseId}}">
<h3 class="margin-bottom-tiny">Documents</h3>
<p class="text-fade">Count of documents over time</p>
<div class="box margin-bottom-small">
@@ -489,7 +505,7 @@ $logs = $this->getParam('logs', null);
</ul>
</div>
</li>
<li data-state="/console/database/collection/settings?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/databases/collection/settings?id={{router.params.id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2>Settings</h2>
<div class="row responsive margin-top-negative">
@@ -500,13 +516,14 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Database Collection"
data-service="database.updateCollection"
data-service="databases.updateCollection"
data-scope="sdk"
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-database-id="{{router.params.databaseId}}"
data-success="alert,trigger"
data-success-param-alert-text="Updated collection successfully"
data-success-param-trigger-events="database.updateCollection"
data-success-param-trigger-events="databases.updateCollection"
data-failure="alert"
data-failure-param-alert-text="Failed to update collection"
data-failure-param-alert-classname="error">
@@ -577,24 +594,25 @@ $logs = $this->getParam('logs', null);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-collection.dateUpdated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-collection.dateCreated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-collection.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-collection.$createdAt|dateText}}"></span></li>
</ul>
<form
name="database.deleteCollection" class="margin-bottom"
name="databases.deleteCollection" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Database Collection"
data-service="database.deleteCollection"
data-service="databases.deleteCollection"
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-database-id="{{router.params.databaseId}}"
data-confirm="Are you sure you want to delete this collection?"
data-success="alert,trigger,redirect"
data-success-param-alert-text="Collection deleted successfully"
data-success-param-trigger-events="database.deleteCollection"
data-success-param-trigger-events="databases.deleteCollection"
data-success-param-redirect-url="/console/database?project={{router.params.project}}"
data-failure="alert"
data-failure-param-alert-text="Failed to delete collection"
@@ -620,12 +638,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (string)"
data-service="database.createStringAttribute"
data-service="databases.createStringAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -634,6 +652,7 @@ $logs = $this->getParam('logs', null);
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<label for="string-key">Attribute ID</label>
<input id="string-key" type="text" class="full-width" name="key" required autocomplete="off" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$" />
@@ -676,12 +695,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (integer)"
data-service="database.createIntegerAttribute"
data-service="databases.createIntegerAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -689,6 +708,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false, min: null, max: null }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="integer-key">Attribute ID</label>
@@ -741,12 +761,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (float)"
data-service="database.createFloatAttribute"
data-service="databases.createFloatAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -754,6 +774,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false, min: null, max: null }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="float-key">Attribute ID</label>
@@ -806,12 +827,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (email)"
data-service="database.createEmailAttribute"
data-service="databases.createEmailAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -819,6 +840,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="email-key">Attribute ID</label>
@@ -859,12 +881,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (boolean)"
data-service="database.createBooleanAttribute"
data-service="databases.createBooleanAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -872,6 +894,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="boolean-key">Attribute ID</label>
@@ -916,12 +939,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (IP)"
data-service="database.createIpAttribute"
data-service="databases.createIpAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -929,6 +952,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="ip-key">Attribute ID</label>
@@ -969,12 +993,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (url)"
data-service="database.createUrlAttribute"
data-service="databases.createUrlAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -982,6 +1006,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="url-key">Attribute ID</label>
@@ -1022,12 +1047,12 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (enum)"
data-service="database.createEnumAttribute"
data-service="databases.createEnumAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="database.createAttribute"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@@ -1035,6 +1060,7 @@ $logs = $this->getParam('logs', null);
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="enum-key">Attribute ID</label>
@@ -1102,17 +1128,18 @@ $logs = $this->getParam('logs', null);
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Index"
data-service="database.createIndex"
data-service="databases.createIndex"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new index successfully"
data-success-param-trigger-events="database.createIndex"
data-success-param-trigger-events="databases.createIndex"
data-failure="alert"
data-failure-param-alert-text="Failed to create index"
data-failure-param-alert-classname="error">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<label for="index-key">Index Key</label>
@@ -1134,8 +1161,15 @@ $logs = $this->getParam('logs', null);
<div data-forms-clone="" data-label="Add Attribute" data-target="attributes-section" data-first="1">
<div class="row responsive thin margin-bottom-tiny">
<div class="col span-7 margin-bottom-small">
<select data-duplications data-ls-attrs="name=attributes" data-ls-loop="project-collection.attributes" data-ls-as="option" data-cast-to="array" class="margin-bottom-no">
<option data-ls-attrs="value={{option.key}}" data-ls-bind="{{option.key}}"></option>
<select data-duplications data-ls-attrs="name=attributes" data-cast-to="array" class="margin-bottom-no">
<optgroup label="Internal">
<option value="$id">$id</option>
<option value="$createdAt">$createdAt</option>
<option value="$updatedAt">$updatedAt</option>
</optgroup>
<optgroup label="Attributes" data-ls-loop="project-collection.attributes" data-ls-as="option">
<option data-ls-attrs="value={{option.key}}" data-ls-bind="{{option.key}}"></option>
</optgroup>
</select>
</div>
<div class="col span-4 margin-bottom-small">
@@ -1157,7 +1191,7 @@ $logs = $this->getParam('logs', null);
</form>
</div>
<div class="margin-top" data-service="database.listCollections" data-event="load,database.createCollection,database.updateCollection,database.deleteCollection" data-scope="sdk" data-param-limit="100" data-name="project-collections">
<div class="margin-top" data-service="databases.listCollections" data-param-database-id="{{router.params.databaseId}}" data-event="load,databases.createCollection,databases.updateCollection,databases.deleteCollection" data-scope="sdk" data-param-limit="100" data-name="project-collections">
</div>
<script type="text/html" id="template-attribute-title-first">
+326
View File
@@ -0,0 +1,326 @@
<div
data-service="databases.get"
data-param-database-id="{{router.params.id}}"
data-scope="sdk"
data-event="load,databases.update"
data-name="project-database">
<div class="cover">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/databases?project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> Database</a>
<br />
<span data-ls-bind="{{project-database.name}}">&nbsp;&nbsp;</span>
</h1>
</div>
<div data-ui-modal class="modal width-large box close" data-button-hide="on" data-open-event="open-json">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>JSON View</h2>
<div class="margin-bottom">
<input type="hidden" data-ls-bind="{{project-database}}" data-forms-code />
</div>
<button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</div>
<div class="zone xl">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/databases/database?id={{router.params.id}}&project={{router.params.project}}">
<h2>Collections</h2>
<div class="margin-top"
data-service="databases.listCollections"
data-event="load,databases.createCollection,databases.updateCollection,databases.deleteCollection"
data-param-database-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections">
<div data-ls-if="(!{{project-collections.total}})" class="box dashboard margin-bottom">
<div class="margin-bottom-small margin-top-small margin-end margin-start">
<h3 class="margin-bottom-small text-bold">No Collections Found</h3>
<p class="margin-bottom-no">You haven't created any collections for your project yet.</p>
</div>
</div>
<div data-ls-if="0 != {{project-collections.total}}">
<ul data-ls-loop="project-collections.collections" data-ls-as="collection" data-ls-append="" class="tiles cell-3 margin-bottom-small">
<li class="margin-bottom">
<a data-ls-attrs="href=/console/databases/collection?id={{collection.$id}}&databaseId={{router.params.id}}&project={{router.params.project}}" class="box">
<div data-ls-bind="{{collection.name}}" class="text-one-liner margin-bottom text-bold">&nbsp;</div>
<i class="icon-right-open"></i>
</a>
</li>
</ul>
</div>
<div class="pull-end text-align-center paging">
<form
data-service="databases.listCollections"
data-param-database-id="{{router.params.id}}"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-collections.total|pageTotal}}"></span>
<form
data-service="databases.listCollections"
data-param-database-id="{{router.params.id}}"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-text="Add Collection">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>New Collection</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Database Collection"
data-service="databases.createCollection"
data-event="submit"
data-scope="sdk"
data-success="alert,reset,redirect,trigger"
data-success-param-alert-text="Collection created successfully"
data-success-param-redirect-url="/console/databases/collection/settings?id={{serviceData.$id}}&databaseId={{router.params.id}}&project={{router.params.project}}"
data-success-param-trigger-events="databases.createCollection"
data-failure="alert"
data-failure-param-alert-text="Failed to create collection"
data-failure-param-alert-classname="error">
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.id}}" />
<label for="collection-id">Collection ID</label>
<input
type="hidden"
data-custom-id
data-id-type="auto"
data-validator="databases.getCollection"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"
name="collectionId" />
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</li>
<li data-state="/console/databases/database/usage?project={{router.params.project}}&id={{router.params.id}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="databases.getDatabaseUsage"
data-event="submit"
data-name="usage"
data-param-database-id="{{router.params.id}}"
data-param-range="90d">
<button class="tick">90d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="databases.getDatabaseUsage"
data-param-database-id="{{router.params.id}}"
data-event="submit"
data-name="usage">
<button class="tick">30d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="databases.getDatabaseUsage"
data-event="submit"
data-name="usage"
data-param-database-id="{{router.params.id}}"
data-param-range="24h">
<button class="tick">24h</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
<h2>Usage</h2>
<div
data-service="databases.getDatabaseUsage"
data-param-database-id="{{router.params.id}}"
data-event="load"
data-name="usage">
<h3 class="margin-bottom-tiny">Objects</h3>
<p class="text-fade">Count of collections and documents over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Collections=collectionsCount,Documents=documentsCount"
data-show-y-axis="true"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Total Collections <span data-ls-bind="({{usage.collectionsCount|statsGetLast|statsTotal}})"></span></li>
<li>Total Documents <span data-ls-bind="({{usage.documentsCount|statsGetLast|statsTotal}})"></span></li>
</ul>
<h3 class="margin-bottom-tiny">Collections</h3>
<p class="text-fade">Count of collections create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=collectionsCreate,Read=collectionsRead,Updated=collectionsUpdate,Deleted=collectionsDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
<h3 class="margin-bottom-tiny">Documents</h3>
<p class="text-fade">Count of documents create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=documentsCreate,Read=documentsRead,Updated=documentsUpdate,Deleted=documentsDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
</div>
</li>
<li data-state="/console/databases/database/settings?id={{router.params.id}}&project={{router.params.project}}">
<h2>Settings</h2>
<div class="row responsive margin-top-negative">
<div class="col span-8 margin-bottom">
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Database"
data-service="databases.update"
data-scope="sdk"
data-event="submit"
data-param-database-id="{{router.params.id}}"
data-success="alert,trigger"
data-success-param-alert-text="Updated database successfully"
data-success-param-trigger-events="databases.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update database"
data-failure-param-alert-classname="error">
<label>&nbsp;</label>
<div class="box">
<label for="database-name">Name</label>
<input name="name" id="database-name" type="text" autocomplete="off" data-ls-bind="{{project-database.name}}" data-forms-text-direction required placeholder="Database Name" maxlength="128" />
<hr class="margin-top-no" />
<button>Update</button>
</form>
</div>
</div>
<div class="col span-4 sticky-top">
<label>Database ID</label>
<div class="input-copy margin-bottom">
<input id="id" type="text" autocomplete="off" placeholder="" data-ls-bind="{{project-database.$id}}" disabled data-forms-copy class="margin-bottom-no" />
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-json" class="link text-size-small">View as JSON</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-database.dateUpdated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-database.dateCreated|dateText}}"></span></li>
</ul>
<form
name="databases.delete" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Database"
data-service="databases.delete"
data-event="submit"
data-param-database-id="{{router.params.id}}"
data-confirm="Are you sure you want to delete this Database?"
data-success="alert,trigger,redirect"
data-success-param-alert-text="Database deleted successfully"
data-success-param-trigger-events="databases.delete"
data-success-param-redirect-url="/console/databases?project={{router.params.project}}"
data-failure="alert"
data-failure-param-alert-text="Failed to delete database"
data-failure-param-alert-classname="error">
<button type="submit" class="danger fill">Delete Database</button>
</form>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
@@ -5,15 +5,17 @@ $logs = $this->getParam('logs', null);
?>
<div
data-service="database.getCollection"
data-service="databases.getCollection"
data-param-collection-id="{{router.params.collection}}"
data-param-database-id="{{router.params.databaseId}}"
data-scope="sdk"
data-event="load,database.updateDocument"
data-event="load,databases.updateDocument"
data-name="project-collection">
<div
data-service="database.getDocument"
data-service="databases.getDocument"
data-param-collection-id="{{router.params.collection}}"
data-param-database-id="{{router.params.databaseId}}"
data-param-document-id="{{router.params.id}}"
data-scope="sdk"
data-event="load"
@@ -22,7 +24,7 @@ $logs = $this->getParam('logs', null);
<div class="cover">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/database/collection?id={{router.params.collection}}&project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> <span data-ls-bind="{{project-collection.name}}"></span></a>
<a data-ls-attrs="href=/console/databases/collection?id={{router.params.collection}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> <span data-ls-bind="{{project-collection.name}}"></span></a>
<br />
@@ -45,7 +47,7 @@ $logs = $this->getParam('logs', null);
<div class="zone xl margin-bottom-no">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/database/document?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<li data-state="/console/databases/document?id={{router.params.id}}&collection={{router.params.collection}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}">
<h2 class="margin-bottom">Overview</h2>
<div class="row responsive">
@@ -64,19 +66,20 @@ $logs = $this->getParam('logs', null);
<?php if($new): ?>
data-analytics-label="Create Database Document"
data-success="trigger,redirect"
data-success-param-trigger-events="database.createDocument"
data-success-param-redirect-url="/console/database/collection?id={{project-collection.$id}}&project={{router.params.project}}"
data-success-param-trigger-events="databases.createDocument"
data-success-param-redirect-url="/console/databases/collection?id={{project-collection.$id}}&databaseId={{router.params.databaseId}}&project={{router.params.project}}"
data-failure-param-alert-text="Failed to create document"
<?php else: ?>
data-analytics-label="Update Database Document"
data-success="trigger,alert"
data-success-param-trigger-events="database.updateDocument"
data-success-param-trigger-events="databases.updateDocument"
data-success-param-alert-text="Your document was updated"
data-failure-param-alert-text="Failed to update document"
<?php endif; ?>
>
<input type="hidden" name="collectionId" data-ls-bind="{{project-collection.$id}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<?php if(!$new): ?><input type="hidden" name="documentId" data-ls-bind="{{project-document.$id}}" /><?php endif; ?>
<div class="box">
@@ -86,7 +89,7 @@ $logs = $this->getParam('logs', null);
type="hidden"
data-custom-id
data-id-type="auto"
data-validator="database.getDocument"
data-validator="databases.getDocument"
required
maxlength="36"
name="documentId"
@@ -356,24 +359,27 @@ $logs = $this->getParam('logs', null);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-document.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-document.$createdAt|dateText}}"></span></li>
</ul>
<div data-ls-if="({{project-document.$id}})">
<form name="database.deleteDocument" class="margin-bottom"
<form name="databases.deleteDocument" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Collection Document"
data-service="database.deleteDocument"
data-service="databases.deleteDocument"
data-event="submit"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.collection}}"
data-param-document-id="{{project-document.$id}}"
data-confirm="Are you sure you want to delete this document?"
data-success="alert,trigger,redirect"
data-success-param-alert-text="Document deleted successfully"
data-success-param-trigger-events="database.deleteDocument"
data-success-param-redirect-url="/console/database/collection?id={{router.params.collection}}&project={{router.params.project}}"
data-success-param-trigger-events="databases.deleteDocument"
data-success-param-redirect-url="/console/databases/collection?id={{router.params.collection}}&project={{router.params.project}}&databaseId={{router.params.databaseId}}"
data-failure="alert"
data-failure-param-alert-text="Failed to delete collection"
data-failure-param-alert-classname="error">
@@ -385,7 +391,7 @@ $logs = $this->getParam('logs', null);
</div>
</li>
<?php if(!$new): ?>
<li data-state="/console/database/document/activity?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<li data-state="/console/databases/document/activity?id={{router.params.id}}&collection={{router.params.collection}}&project={{router.params.project}}">
<h2>Activity</h2>
<?php echo $logs->render(); ?>
@@ -9,32 +9,32 @@
<div class="zone xl">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/database?project={{router.params.project}}">
<h2>Collections</h2>
<li data-state="/console/databases?project={{router.params.project}}">
<h2>Databases</h2>
<div class="margin-top"
data-service="database.listCollections"
data-event="load,database.createCollection,database.updateCollection,database.deleteCollection"
data-service="databases.list"
data-event="load,databases.create,databases.update,databases.delete"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections">
data-name="project-databases">
<div data-ls-if="(!{{project-collections.total}})" class="box dashboard margin-bottom">
<div data-ls-if="(!{{project-databases.total}})" class="box dashboard margin-bottom">
<div class="margin-bottom-small margin-top-small margin-end margin-start">
<h3 class="margin-bottom-small text-bold">No Collections Found</h3>
<h3 class="margin-bottom-small text-bold">No Databases Found</h3>
<p class="margin-bottom-no">You haven't created any collections for your project yet.</p>
<p class="margin-bottom-no">You haven't created any databases for your project yet.</p>
</div>
</div>
<div data-ls-if="0 != {{project-collections.total}}">
<ul data-ls-loop="project-collections.collections" data-ls-as="collection" data-ls-append="" class="tiles cell-3 margin-bottom-small">
<div data-ls-if="0 != {{project-databases.total}}">
<ul data-ls-loop="project-databases.databases" data-ls-as="database" data-ls-append="" class="tiles cell-3 margin-bottom-small">
<li class="margin-bottom">
<a data-ls-attrs="href=/console/database/collection?id={{collection.$id}}&project={{router.params.project}}" class="box">
<div data-ls-bind="{{collection.name}}" class="text-one-liner margin-bottom text-bold">&nbsp;</div>
<a data-ls-attrs="href=/console/databases/database?id={{database.$id}}&project={{router.params.project}}" class="box">
<div data-ls-bind="{{database.name}}" class="text-one-liner margin-bottom text-bold">&nbsp;</div>
<i class="icon-right-open"></i>
</a>
@@ -44,84 +44,79 @@
<div class="pull-end text-align-center paging">
<form
data-service="database.listCollections"
data-service="databases.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-name="project-databases"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-collections.total|pageTotal}}"></span>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-databases.total|pageTotal}}"></span>
<form
data-service="database.listCollections"
data-service="databases.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-name="project-databases"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-text="Add Collection">
<div data-ui-modal class="modal close box sticky-footer" data-button-text="Add Database">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>New Collection</h1>
<h1>New Database</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Database Collection"
data-service="database.createCollection"
data-analytics-label="Create Database"
data-service="databases.create"
data-event="submit"
data-scope="sdk"
data-success="alert,reset,redirect,trigger"
data-success-param-alert-text="Collection created successfully"
data-success-param-redirect-url="/console/database/collection/settings?id={{serviceData.$id}}&project={{router.params.project}}"
data-success-param-trigger-events="database.createCollection"
data-success-param-alert-text="Database created successfully"
data-success-param-redirect-url="/console/databases/database?id={{serviceData.$id}}&project={{router.params.project}}"
data-success-param-trigger-events="databases.create"
data-failure="alert"
data-failure-param-alert-text="Failed to create collection"
data-failure-param-alert-text="Failed to create database"
data-failure-param-alert-classname="error">
<label for="collection-id">Collection ID</label>
<label for="database-id">Database ID</label>
<input
type="hidden"
data-custom-id
data-id-type="auto"
data-validator="database.getCollection"
data-validator="databases.get"
required
maxlength="36"
pattern="^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"
name="collectionId" />
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
name="databaseId" />
<label for="database-name">Name</label>
<input type="text" class="full-width" id="database-name" name="name" required autocomplete="off" maxlength="128" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</li>
<li data-state="/console/database/usage?project={{router.params.project}}">
</li><!-- TODO need to work on usage -->
<li data-state="/console/databases/usage?project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="database.getUsage"
data-service="databases.getUsage"
data-event="submit"
data-name="usage"
data-param-range="90d">
@@ -131,7 +126,7 @@
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="database.getUsage"
data-service="databases.getUsage"
data-event="submit"
data-name="usage">
<button class="tick">30d</button>
@@ -140,7 +135,7 @@
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="database.getUsage"
data-service="databases.getUsage"
data-event="submit"
data-name="usage"
data-param-range="24h">
@@ -152,7 +147,7 @@
<h2>Usage</h2>
<div
data-service="database.getUsage"
data-service="databases.getUsage"
data-event="load"
data-name="usage">
<h3 class="margin-bottom-tiny">Objects</h3>
@@ -163,7 +158,7 @@
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Collections=collectionsCount,Documents=documentsCount"
data-forms-chart="Databases=databasesCount,Collections=collectionsCount,Documents=documentsCount"
data-show-y-axis="true"
data-height="140" />
</div>
@@ -171,10 +166,33 @@
</div>
<ul class="chart-notes margin-bottom-large">
<li>Total Databases <span data-ls-bind="({{usage.databasesCount|statsGetLast|statsTotal}})"></span></li>
<li>Total Collections <span data-ls-bind="({{usage.collectionsCount|statsGetLast|statsTotal}})"></span></li>
<li>Total Documents <span data-ls-bind="({{usage.documentsCount|statsGetLast|statsTotal}})"></span></li>
</ul>
<h3 class="margin-bottom-tiny">Databases</h3>
<p class="text-fade">Count of databases create, read, update and delete operations over time</p>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=databasesCreate,Read=databasesRead,Updated=databasesUpdate,Deleted=databasesDelete"
data-show-y-axis="true"
data-colors="create,read,update,delete"
data-height="140" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large crud">
<li>Created</li>
<li>Read</li>
<li>Updated</li>
<li>Deleted</li>
</ul>
<h3 class="margin-bottom-tiny">Collections</h3>
<p class="text-fade">Count of collections create, read, update and delete operations over time</p>
+19 -9
View File
@@ -4,7 +4,12 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
$events = $this->getParam('events', []);
$timeout = $this->getParam('timeout', 900);
$usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
$patterns = [];
$patterns = [
'documents',
'documents.create',
'documents.update',
'documents.delete',
];
foreach ($events as $name => $event) {
$patterns[] = $name;
@@ -170,7 +175,7 @@ sort($patterns);
<span data-ls-if="{{deployment.status}} == 'failed'" style="color: var(--config-color-danger)" class="pull-start" data-ls-bind="{{deployment.status}}"></span>
<span data-ls-if="{{deployment.status}} == 'ready'" style="color: var(--config-color-success)" class="pull-start" data-ls-bind="{{deployment.status}}"></span>
<span data-ls-if="{{deployment.status}} == 'processing' || {{deployment.status}} == 'building'" style="color: var(--config-color-info)" class="pull-start" data-ls-bind="{{deployment.status}}"></span>
<span class="pull-start" data-ls-bind="&nbsp; | &nbsp; Created {{deployment.dateCreated|timeSince}} &nbsp; | &nbsp; {{deployment.size|humanFileSize}}{{deployment.size|humanFileUnit}}"></span>
<span class="pull-start" data-ls-bind="&nbsp; | &nbsp; Created {{deployment.$createdAt|timeSince}} &nbsp; | &nbsp; {{deployment.size|humanFileSize}}{{deployment.size|humanFileUnit}}"></span>
<span data-ls-if="{{deployment.status}} == 'failed'">&nbsp; | &nbsp;<button type="button" class="link text-size-small" data-ls-ui-trigger="open-stderr-{{deployment.$id}}">Logs</button></span>
<span data-ls-if="{{deployment.status}} == 'ready'">&nbsp; | &nbsp;<button type="button" class="link text-size-small" data-ls-ui-trigger="open-stdout-{{deployment.$id}}">Logs</button></span>
@@ -245,7 +250,7 @@ sort($patterns);
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
data-analytics
@@ -255,8 +260,8 @@ sort($patterns);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.dateUpdated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.dateCreated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|dateText}}"></span></li>
</ul>
<form name="functions.delete" class="margin-bottom"
@@ -402,7 +407,7 @@ sort($patterns);
<i class="dot success" data-ls-if="{{execution.status}} === 'completed'"></i>
</td>
<td data-title="Date: ">
<span data-ls-bind="{{execution.dateCreated|dateTime}}"></span>
<span data-ls-bind="{{execution.$createdAt|dateTime}}"></span>
</td>
<td data-title="Status: ">
<span data-ls-bind="{{execution.status}}"></span>
@@ -604,7 +609,7 @@ sort($patterns);
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
data-analytics
@@ -614,8 +619,8 @@ sort($patterns);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.dateUpdated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.dateCreated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|dateText}}"></span></li>
</ul>
<form name="functions.delete" class="margin-bottom"
@@ -662,6 +667,11 @@ sort($patterns);
<input id="subResource" type="text" :placeholder="subResourceName" x-model="subResource" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{0,35}$">
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Leave empty for wildcard</div>
</div>
<div x-show="hasSubSubResource">
<label x-text="subSubResourceName + ' (optional)'" for="subSubResource"></label>
<input id="subSubResource" type="text" :placeholder="subSubResourceName" x-model="subSubResource" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{0,35}$">
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Leave empty for wildcard</div>
</div>
<div x-show="hasAttribute">
<label for="attribute">Add Attribute (optional)</label>
<select id="attribute" x-model="attribute">
+5 -5
View File
@@ -190,7 +190,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<span data-ls-bind="{{file.sizeOriginal|humanFileUnit}}"></span>
</div>
<div class="margin-bottom">
<i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Created at: <span data-ls-bind="{{file.dateCreated|dateText}}"></span>
<i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Created at: <span data-ls-bind="{{file.$createdAt|dateText}}"></span>
</div>
</div>
</div>
@@ -211,7 +211,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<span class="text-fade text-size-small" data-ls-bind="{{file.sizeOriginal|humanFileUnit}}"></span>
</td>
<td data-title="Created: ">
<span class="text-fade text-size-small" data-ls-bind="{{file.dateCreated|dateText}}"></span>
<span class="text-fade text-size-small" data-ls-bind="{{file.$createdAt|dateText}}"></span>
</td>
<td data-title="" class="cell-options-more" style="overflow: visible">
<div class="drop-list end" data-ls-ui-open="" data-button-aria="File Options" data-button-class="icon-dot-3 reset-inner-button" data-blur="1">
@@ -471,7 +471,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
data-analytics
@@ -481,8 +481,8 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-bucket.dateUpdated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-bucket.dateCreated|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-bucket.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-bucket.$createdAt|dateText}}"></span></li>
</ul>
<form name="storage.deleteBucket" class="margin-bottom"
+6 -6
View File
@@ -80,19 +80,19 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<td data-title="Name: ">
<a data-ls-attrs="href=/console/users/user?id={{user.$id}}&project={{router.params.project}}">
<span data-ls-bind="{{user.name}}" data-ls-attrs="title={{user.name}}"></span>
<span data-ls-if="{{user.name|escape}} === '' && {{user.email}} !== ''">Unknown</span>
<span data-ls-if="{{user.name|escape}} === '' && {{user.email}} === ''">Anonymous User</span>
<span data-ls-if="{{user.name|escape}} === '' && ({{user.email}} !== '' || {{user.phone}} !== '')">Unknown</span>
<span data-ls-if="{{user.name|escape}} === '' && {{user.email}} === '' && {{user.phone}} === ''">Anonymous User</span>
</a>
</td>
<td data-title="Email: ">
<small data-ls-bind="{{user.email}}" data-ls-attrs="title={{user.email}}"></span>
<span data-ls-bind="{{user.email}}" data-ls-attrs="title={{user.email}}"></span>
</td>
<td data-title="Status: ">
<span data-ls-if="{{user.emailVerification}} === true && {{user.status}} === true">
<span data-ls-if="({{user.emailVerification}} || {{user.phoneVerification}}) && {{user.status}} === true">
<span class="tag green">Verified</span>
</span>
<span data-ls-if="{{user.emailVerification}} !== true && {{user.status}} === true">
<span data-ls-if="!({{user.emailVerification}} || {{user.phoneVerification}}) && {{user.status}} === true">
<span class="tag">Unverified</span>
</span>
@@ -248,7 +248,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<a data-ls-attrs="href=/console/users/teams/team?id={{team.$id}}&project={{router.params.project}}" data-ls-bind="{{team.name}}" data-ls-attrs="title={{team.name}}"></a>
</td>
<td data-title="Members: "><span data-ls-bind="{{team.total}} members"></span></td>
<td data-title="Date Created: "><small data-ls-bind="{{team.dateCreated|dateText}}"></small></td>
<td data-title="Date Created: "><small data-ls-bind="{{team.$createdAt|dateText}}"></small></td>
</tr>
</tbody>
</table>
+3 -3
View File
@@ -214,9 +214,9 @@
data-analytics-category="console"
data-analytics-label="View as JSON (Team)">
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{team.dateCreated|dateText}}"></span></li>
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{team.$createdAt|dateText}}"></span></li>
</ul>
<form name="teams.delete" class="margin-bottom"
+109 -16
View File
@@ -20,7 +20,7 @@
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-name">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>Update name</h2>
<h2>Update Name</h2>
<form name="users.updateName"
data-analytics
@@ -85,6 +85,41 @@
</form>
</div>
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-phone">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h2>Update Phone</h2>
<form name="users.updatePhone"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update User Phone"
data-service="users.updatePhone"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User phone was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user email"
data-failure-param-alert-classname="error">
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<label for="number">Phone number</label>
<input name="number" id="number" type="text" autocomplete="off" data-ls-bind="{{user.phone}}" pattern="^\+?[1-9]\d{1,14}$" class="full-width" title="Phone number with a leading '+' and maximum of 15 digits (+123456789).">
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Phone number with a leading '+' and maximum of 15 digits (+123456789)</div>
<hr />
<footer>
<button type="submit" class="">Update</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-password">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
@@ -144,20 +179,31 @@
<div class="col span-8">
<label>&nbsp;</label>
<div class="box margin-bottom-large">
<div class="box margin-bottom-large" style="padding-bottom: 5px">
<div class="text-align-center">
<img src="" data-ls-attrs="src={{user|avatar}}" data-size="200" alt="User Avatar" class="avatar huge margin-top-negative-xxl" />
<div class="margin-top-small" data-ls-bind="Member since {{user.registration|dateText}}"></div>
<div class="margin-top-small">
<span data-ls-if="{{user.emailVerification}} === true">
<div class="margin-top-small margin-bottom-small" data-ls-bind="Member since {{user.registration|dateText}}"></div>
<hr class="margin-top-tiny margin-bottom-tiny" data-ls-if="{{user.email}}">
<div class="margin-top-small margin-bottom-small clear" data-ls-if="{{user.email}}">
<span data-ls-bind="{{user.email}}" class="pull-start"></span>
<span data-ls-if="{{user.emailVerification}} === true" class="pull-end">
<span class="tag green">Verified</span>
</span>
<span data-ls-if="{{user.emailVerification}} !== true">
<span data-ls-if="{{user.emailVerification}} !== true" class="pull-end">
<span class="tag">Unverified</span>
</span>
</div>
<hr class="margin-top-tiny margin-bottom-tiny" data-ls-if="{{user.phone}}">
<div class="margin-top-small margin-bottom-small clear" data-ls-if="{{user.phone}}">
<span data-ls-bind="{{user.phone}}" class="pull-start"></span>
<span data-ls-if="{{user.phoneVerification}} === true" class="pull-end">
<span class="tag green">Verified</span>
</span>
<span data-ls-if="{{user.phoneVerification}} !== true" class="pull-end">
<span class="tag">Unverified</span>
</span>
</div>
<div class="margin-top-small" data-ls-bind="{{user.email}}"></div>
</div>
</div>
@@ -236,8 +282,9 @@
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-name" class="link text-size-small">Update name</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-name" class="link text-size-small">Update Name</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-email" class="link text-size-small">Update Email</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-phone" class="link text-size-small">Update Phone</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-password" class="link text-size-small">Update Password</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
@@ -251,14 +298,14 @@
</li>
</ul>
<div data-ls-if="{{user.emailVerification}} === false" style="display: none">
<div data-ls-if="{{user.email}} && {{user.emailVerification}} === false" style="display: none">
<form name="users.updateVerification" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Verification Status"
data-service="users.updateVerification"
data-analytics-label="Update Email Verification Status"
data-service="users.updateEmailVerification"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
@@ -270,18 +317,18 @@
>
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<input type="hidden" disabled name="emailVerification" value="true" data-cast-to="boolean">
<button type="submit" class="dark fill">Verify Account</button>
<button type="submit" class="dark fill">Verify Email</button>
</form>
</div>
<div data-ls-if="{{user.emailVerification}} === true" style="display: none">
<div data-ls-if="{{user.email}} && {{user.emailVerification}} === true" style="display: none">
<form name="users.updateVerification" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Verification Status"
data-service="users.updateVerification"
data-analytics-label="Update Email Verification Status"
data-service="users.updateEmailVerification"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
@@ -293,7 +340,53 @@
>
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<input type="hidden" disabled name="emailVerification" value="false" data-cast-to="boolean">
<button type="submit" class="dark fill">Unverify Account</button>
<button type="submit" class="dark fill">Unverify Email</button>
</form>
</div>
<div data-ls-if="{{user.phone}} && {{user.phoneVerification}} === false" style="display: none">
<form name="users.updateVerification" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Phone Verification Status"
data-service="users.updatePhoneVerification"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User verification status was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user verification status"
data-failure-param-alert-classname="error"
>
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<input type="hidden" disabled name="phoneVerification" value="true" data-cast-to="boolean">
<button type="submit" class="dark fill">Verify Phone</button>
</form>
</div>
<div data-ls-if="{{user.phone}} && {{user.phoneVerification}} === true" style="display: none">
<form name="users.updateVerification" class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Phone Verification Status"
data-service="users.updatePhoneVerification"
data-scope="sdk"
data-event="submit"
data-success="trigger,alert"
data-success-param-alert-text="User verification status was updated successfully"
data-success-param-trigger-events="users.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update user verification status"
data-failure-param-alert-classname="error"
>
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
<input type="hidden" disabled name="phoneVerification" value="false" data-cast-to="boolean">
<button type="submit" class="dark fill">Unverify Phone</button>
</form>
</div>
+59 -3
View File
@@ -1,7 +1,12 @@
<?php
$new = $this->getParam('new', false);
$events = $this->getParam('events', []);
$patterns = [];
$patterns = [
'documents',
'documents.create',
'documents.update',
'documents.delete',
];
foreach ($events as $name => $event) {
$patterns[] = $name;
@@ -30,7 +35,7 @@ sort($patterns);
data-service="projects.getWebhook"
data-name="project-webhook"
data-scope="console"
data-event="load,projects.createWebhook, projects.deleteWebhook, projects.updateWebhook"
data-event="load,projects.createWebhook,projects.deleteWebhook,projects.updateWebhook,projects.updateWebhookSignature"
data-param-project-id="{{router.params.project}}"
data-param-webhook-id="{{router.params.id}}"
data-success="trigger"
@@ -156,7 +161,53 @@ sort($patterns);
<input id="uid" type="text" autocomplete="off" placeholder="" data-ls-bind="{{project-webhook.$id}}" disabled data-forms-copy>
</div>
<form class="margin-bottom" data-analytics data-analytics-activity data-analytics-event="submit" data-analytics-category="console" data-analytics-label="Delete Project Webhook" data-service="projects.deleteWebhook" data-scope="console" data-event="submit" data-confirm="Are you sure you want to delete this webhook?" data-success="alert,redirect" data-success-param-redirect-url="/console/webhooks?project={{router.params.project}}" data-success-param-alert-text="Deleted webhook successfully" data-success-param-trigger-events="projects.deleteWebhook" data-failure="alert" data-failure-param-alert-text="Failed to delete webhook" data-failure-param-alert-classname="error">
<label>Signature Key <span class="tooltip small" data-tooltip="Can be used to validate your Webhooks."><i class="icon-info-circled"></i></span></label>
<div class="input-copy margin-bottom">
<input id="signatureKey" type="text" autocomplete="off" placeholder="" data-ls-bind="{{project-webhook.signatureKey}}" disabled data-forms-copy>
</div>
<form
class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Project Webhook Signature"
data-service="projects.updateWebhookSignature"
data-scope="console"
data-event="submit"
data-confirm="Are you sure you want to generate a new Signature Key?"
data-success="trigger"
data-success-param-alert-text="Updated webhook signature key successfully"
data-success-param-trigger-events="projects.updateWebhookSignature"
data-failure="alert"
data-failure-param-alert-text="Failed to update webhook signature key"
data-failure-param-alert-classname="error">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="webhookId" data-ls-bind="{{project-webhook.$id}}" />
<button class="fill">Update Signature Key</button>
</form>
<form
class="margin-bottom"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Project Webhook"
data-service="projects.deleteWebhook"
data-scope="console"
data-event="submit"
data-confirm="Are you sure you want to delete this webhook?"
data-success="alert,redirect"
data-success-param-redirect-url="/console/webhooks?project={{router.params.project}}"
data-success-param-alert-text="Deleted webhook successfully"
data-success-param-trigger-events="projects.deleteWebhook"
data-failure="alert"
data-failure-param-alert-text="Failed to delete webhook"
data-failure-param-alert-classname="error">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="webhookId" data-ls-bind="{{project-webhook.$id}}" />
@@ -187,6 +238,11 @@ sort($patterns);
<input id="subResource" type="text" :placeholder="subResourceName" x-model="subResource" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{0,35}$">
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Leave empty for wildcard</div>
</div>
<div x-show="hasSubSubResource">
<label x-text="subSubResourceName + ' (optional)'" for="subSubResource"></label>
<input id="subSubResource" type="text" :placeholder="subSubResourceName" x-model="subSubResource" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9_-]{0,35}$">
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Leave empty for wildcard</div>
</div>
<div x-show="hasAttribute">
<label for="attribute">Add Attribute (optional)</label>
<select id="attribute" x-model="attribute">
+3 -3
View File
@@ -19,17 +19,17 @@ $root = ($this->getParam('root') !== 'disabled');
<p>Login using email and password</p>
<form name="account.createSession"
<form name="account.createEmailSession"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="home"
data-analytics-label="Create Account Session"
data-service="account.createSession"
data-service="account.createEmailSession"
data-scope="console"
data-event="submit"
data-success="trigger,hide,redirect"
data-success-param-trigger-events="account.createSession"
data-success-param-trigger-events="account.createEmailSession"
data-success-param-redirect-url="/console"
data-failure="alert"
data-failure-param-alert-text="Login failed. Please check your credentials."
+27 -3
View File
@@ -149,6 +149,8 @@ services:
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
appwrite-realtime:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@@ -295,11 +297,11 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
appwrite-worker-database:
appwrite-worker-databases:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-database
entrypoint: worker-databases
<<: *x-logging
container_name: appwrite-worker-database
container_name: appwrite-worker-databases
restart: unless-stopped
networks:
- appwrite
@@ -497,6 +499,26 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-messaging:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
networks:
- appwrite
depends_on:
- redis
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-maintenance:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: maintenance
@@ -552,6 +574,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-schedule:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
-3
View File
@@ -15,9 +15,6 @@ use Utopia\Config\Config;
require_once __DIR__ . '/../init.php';
// Disable Auth since we already validate it in the API
Authorization::disable();
Console::title('Builds V1 Worker');
Console::success(APP_NAME . ' build worker v1 has started');
-2
View File
@@ -36,8 +36,6 @@ class CertificatesV1 extends Worker
public function run(): void
{
Authorization::disable();
Authorization::setDefaultStatus(false);
/**
* 1. Read arguments and validate domain
* 2. Get main domain
@@ -5,7 +5,6 @@ use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
require_once __DIR__ . '/../init.php';
@@ -20,12 +19,11 @@ class DatabaseV1 extends Worker
public function run(): void
{
Authorization::disable();
$type = $this->args['type'];
$project = new Document($this->args['project']);
$collection = new Document($this->args['collection'] ?? []);
$document = new Document($this->args['document'] ?? []);
$database = new Document($this->args['database'] ?? []);
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -37,24 +35,22 @@ class DatabaseV1 extends Worker
switch (strval($type)) {
case DATABASE_TYPE_CREATE_ATTRIBUTE:
$this->createAttribute($collection, $document, $project->getId());
$this->createAttribute($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_ATTRIBUTE:
$this->deleteAttribute($collection, $document, $project->getId());
$this->deleteAttribute($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_CREATE_INDEX:
$this->createIndex($collection, $document, $project->getId());
$this->createIndex($database, $collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_INDEX:
$this->deleteIndex($collection, $document, $project->getId());
$this->deleteIndex($database, $collection, $document, $project->getId());
break;
default:
Console::error('No database operation for type: ' . $type);
break;
}
Authorization::reset();
}
public function shutdown(): void
@@ -62,16 +58,18 @@ class DatabaseV1 extends Worker
}
/**
* @param Document $database
* @param Document $collection
* @param Document $attribute
* @param string $projectId
*/
protected function createAttribute(Document $collection, Document $attribute, string $projectId): void
protected function createAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('collections.[collectionId].attributes.[attributeId].update', [
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
@@ -94,7 +92,7 @@ class DatabaseV1 extends Worker
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if (!$dbForProject->createAttribute('collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
throw new Exception('Failed to create Attribute');
}
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
@@ -106,7 +104,7 @@ class DatabaseV1 extends Worker
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project
project: $project,
);
Realtime::send(
@@ -117,25 +115,28 @@ class DatabaseV1 extends Worker
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId);
}
/**
* @param Document $database
* @param Document $collection
* @param Document $attribute
* @param string $projectId
*/
protected function deleteAttribute(Document $collection, Document $attribute, string $projectId): void
protected function deleteAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('collections.[collectionId].attributes.[attributeId].delete', [
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
@@ -151,7 +152,7 @@ class DatabaseV1 extends Worker
// - failed: attribute was never created
// - stuck: attribute was available but cannot be removed
try {
if ($status !== 'failed' && !$dbForProject->deleteAttribute('collection_' . $collection->getInternalId(), $key)) {
if ($status !== 'failed' && !$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
@@ -174,6 +175,7 @@ class DatabaseV1 extends Worker
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
@@ -223,7 +225,7 @@ class DatabaseV1 extends Worker
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($collection, $index, $projectId);
$this->deleteIndex($database, $collection, $index, $projectId);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
@@ -231,21 +233,23 @@ class DatabaseV1 extends Worker
}
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
$dbForProject->deleteCachedCollection('collection_' . $collection->getInternalId());
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId);
$dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
}
/**
* @param Document $database
* @param Document $collection
* @param Document $index
* @param string $projectId
*/
protected function createIndex(Document $collection, Document $index, string $projectId): void
protected function createIndex(Document $database, Document $collection, Document $index, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('collections.[collectionId].indexes.[indexId].update', [
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
@@ -258,7 +262,7 @@ class DatabaseV1 extends Worker
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if (!$dbForProject->createIndex('collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
if (!$dbForProject->createIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
throw new Exception('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
@@ -281,25 +285,28 @@ class DatabaseV1 extends Worker
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId);
}
/**
* @param Document $database
* @param Document $collection
* @param Document $index
* @param string $projectId
*/
protected function deleteIndex(Document $collection, Document $index, string $projectId): void
protected function deleteIndex(Document $database, Document $collection, Document $index, string $projectId): void
{
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$events = Event::generateEvents('collections.[collectionId].indexes.[indexId].delete', [
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
@@ -308,7 +315,7 @@ class DatabaseV1 extends Worker
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if ($status !== 'failed' && !$dbForProject->deleteIndex('collection_' . $collection->getInternalId(), $key)) {
if ($status !== 'failed' && !$dbForProject->deleteIndex('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete index');
}
$dbForProject->deleteDocument('indexes', $index->getId());
@@ -331,11 +338,12 @@ class DatabaseV1 extends Worker
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
$dbForProject->deleteCachedDocument('collections', $collection->getId());
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collection->getId());
}
}
+45 -7
View File
@@ -15,9 +15,6 @@ use Utopia\Audit\Audit;
require_once __DIR__ . '/../init.php';
Authorization::disable();
Authorization::setDefaultStatus(false);
Console::title('Deletes V1 Worker');
Console::success(APP_NAME . ' deletes worker v1 has started' . "\n");
@@ -47,6 +44,9 @@ class DeletesV1 extends Worker
$document = new Document($this->args['document'] ?? []);
switch ($document->getCollection()) {
case DELETE_TYPE_DATABASES:
$this->deleteDatabase($document, $project->getId());
break;
case DELETE_TYPE_COLLECTIONS:
$this->deleteCollection($document, $project->getId());
break;
@@ -79,8 +79,8 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_AUDIT:
$timestamp = $payload['timestamp'] ?? 0;
$document = new Document($payload['document'] ?? []);
$timestamp = $this->args['timestamp'] ?? 0;
$document = new Document($this->args['document'] ?? []);
if (!empty($timestamp)) {
$this->deleteAuditLogs($this->args['timestamp']);
@@ -100,6 +100,10 @@ class DeletesV1 extends Worker
$this->deleteRealtimeUsage($this->args['timestamp']);
break;
case DELETE_TYPE_SESSIONS:
$this->deleteExpiredSessions($this->args['timestamp']);
break;
case DELETE_TYPE_CERTIFICATES:
$document = new Document($this->args['document']);
$this->deleteCertificates($document);
@@ -118,6 +122,25 @@ class DeletesV1 extends Worker
{
}
/**
* @param Document $document database document
* @param string $projectId
*/
protected function deleteDatabase(Document $document, string $projectId): void
{
$databaseId = $document->getId();
$dbForProject = $this->getProjectDB($projectId);
$this->deleteByGroup('database_' . $document->getInternalId(), [], $dbForProject, function ($document) use ($projectId) {
$this->deleteCollection($document, $projectId);
});
$dbForProject->deleteCollection('database_' . $document->getInternalId());
$this->deleteAuditLogsByResource('database/' . $databaseId, $projectId);
}
/**
* @param Document $document teams document
* @param string $projectId
@@ -125,10 +148,11 @@ class DeletesV1 extends Worker
protected function deleteCollection(Document $document, string $projectId): void
{
$collectionId = $document->getId();
$databaseId = str_replace('database_', '', $document->getCollection());
$dbForProject = $this->getProjectDB($projectId);
$dbForProject->deleteCollection('collection_' . $document->getInternalId());
$dbForProject->deleteCollection('database_' . $databaseId . '_collection_' . $document->getInternalId());
$this->deleteByGroup('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$collectionId])
@@ -245,7 +269,21 @@ class DeletesV1 extends Worker
$dbForProject = $this->getProjectDB($projectId);
// Delete Executions
$this->deleteByGroup('executions', [
new Query('dateCreated', Query::TYPE_LESSER, [$timestamp])
new Query('$createdAt', Query::TYPE_LESSER, [$timestamp])
], $dbForProject);
});
}
/**
* @param int $timestamp
*/
protected function deleteExpiredSessions(int $timestamp): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Sessions
$this->deleteByGroup('sessions', [
new Query('expire', Query::TYPE_LESSER, [$timestamp])
], $dbForProject);
});
}
+35 -37
View File
@@ -44,6 +44,10 @@ class FunctionsV1 extends Worker
$user = new Document($this->args['user'] ?? []);
$payload = json_encode($this->args['payload'] ?? []);
if ($project->getId() === 'console') {
return;
}
$database = $this->getProjectDB($project->getId());
/**
@@ -57,7 +61,7 @@ class FunctionsV1 extends Worker
/** @var Document[] $functions */
while ($sum >= $limit) {
$functions = Authorization::skip(fn () => $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]));
$functions = $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]);
$sum = \count($functions);
$offset = $offset + $limit;
@@ -101,7 +105,7 @@ class FunctionsV1 extends Worker
$jwt = $this->args['jwt'] ?? '';
$data = $this->args['data'] ?? '';
$function = Authorization::skip(fn () => $database->getDocument('functions', $execution->getAttribute('functionId')));
$function = $database->getDocument('functions', $execution->getAttribute('functionId'));
$this->execute(
project: $project,
@@ -132,7 +136,7 @@ class FunctionsV1 extends Worker
*/
// Reschedule
$function = Authorization::skip(fn () => $database->getDocument('functions', $function->getId()));
$function = $database->getDocument('functions', $function->getId());
if (empty($function->getId())) {
throw new Exception('Function not found (' . $function->getId() . ')');
@@ -149,11 +153,11 @@ class FunctionsV1 extends Worker
->setAttribute('scheduleNext', $next)
->setAttribute('schedulePrevious', \time());
$function = Authorization::skip(fn () => $database->updateDocument(
$function = $database->updateDocument(
'functions',
$function->getId(),
$function->setAttribute('scheduleNext', (int) $next)
));
);
if ($function === false) {
throw new Exception('Function update failed.');
@@ -198,7 +202,7 @@ class FunctionsV1 extends Worker
$deploymentId = $function->getAttribute('deployment', '');
/** Check if deployment exists */
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->getAttribute('resourceId') !== $functionId) {
throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404);
@@ -209,7 +213,7 @@ class FunctionsV1 extends Worker
}
/** Check if build has exists */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
}
@@ -228,35 +232,30 @@ class FunctionsV1 extends Worker
$runtime = $runtimes[$function->getAttribute('runtime')];
/** Create execution or update execution status */
$execution = Authorization::skip(function () use ($dbForProject, &$executionId, $functionId, $deploymentId, $trigger, $user) {
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
$executionId = $dbForProject->getId();
$execution = $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()],
'$write' => [],
'functionId' => $functionId,
'deploymentId' => $deploymentId,
'trigger' => $trigger,
'status' => 'waiting',
'statusCode' => 0,
'response' => '',
'stderr' => '',
'time' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
]));
if ($execution->isEmpty()) {
$executionId = $dbForProject->getId();
$execution = $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()],
'$write' => [],
'dateCreated' => time(),
'functionId' => $functionId,
'deploymentId' => $deploymentId,
'trigger' => $trigger,
'status' => 'waiting',
'statusCode' => 0,
'response' => '',
'stderr' => '',
'time' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
]));
if ($execution->isEmpty()) {
throw new Exception('Failed to create or read execution');
}
throw new Exception('Failed to create or read execution');
}
$execution->setAttribute('status', 'processing');
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
return $execution;
});
}
$execution->setAttribute('status', 'processing');
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
/** Collect environment variables */
$vars = [
@@ -298,7 +297,7 @@ class FunctionsV1 extends Worker
->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
$endtime = \microtime(true);
$time = $endtime - $execution->getAttribute('dateCreated');
$time = $endtime - $execution->getCreatedAt();
$execution
->setAttribute('time', $time)
->setAttribute('status', 'failed')
@@ -307,8 +306,7 @@ class FunctionsV1 extends Worker
Console::error($th->getMessage());
}
$execution = Authorization::skip(fn () => $dbForProject->updateDocument('executions', $executionId, $execution));
/** @var Document $execution */
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
/** Trigger Webhook */
$executionModel = new Execution();
+70
View File
@@ -0,0 +1,70 @@
<?php
use Appwrite\Auth\Phone;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\DSN\DSN;
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
require_once __DIR__ . '/../init.php';
Console::title('Messaging V1 Worker');
Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
class MessagingV1 extends Worker
{
protected ?Phone $phone = null;
protected ?string $from = null;
public function getName(): string
{
return "mails";
}
public function init(): void
{
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
$this->phone = match ($dsn->getHost()) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
default => null
};
$this->from = App::getEnv('_APP_PHONE_FROM');
}
public function run(): void
{
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
Console::info('Skipped sms processing. No Phone provider has been set.');
return;
}
if (empty($this->from)) {
Console::info('Skipped sms processing. No phone number has been set.');
return;
}
$recipient = $this->args['recipient'];
$message = $this->args['message'];
try {
$this->phone->send($this->from, $recipient, $message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}
}
public function shutdown(): void
{
}
}
+1 -1
View File
@@ -7,4 +7,4 @@ else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=0.1 QUEUE='v1-database' APP_INCLUDE='/usr/src/code/app/workers/database.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
INTERVAL=0.1 QUEUE='v1-database' APP_INCLUDE='/usr/src/code/app/workers/databases.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
then
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
else
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
fi
INTERVAL=1 QUEUE='v1-messaging' APP_INCLUDE='/usr/src/code/app/workers/messaging.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
+4 -4
View File
@@ -36,7 +36,7 @@
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.9.*",
"appwrite/php-runtimes": "0.10.*",
"utopia-php/framework": "0.19.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.7.*",
@@ -45,7 +45,7 @@
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.12.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.17.*",
"utopia-php/database": "0.18.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "dev-feat-allow-querying",
"utopia-php/preloader": "0.2.*",
@@ -63,7 +63,6 @@
"chillerlan/php-qrcode": "4.3.3",
"adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0",
"squizlabs/php_codesniffer": "^3.6",
"webonyx/graphql-php": "14.1.1"
},
"repositories": [
@@ -73,8 +72,9 @@
}
],
"require-dev": {
"appwrite/sdk-generator": "0.18.8",
"appwrite/sdk-generator": "0.19.4",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.6",
"swoole/ide-helper": "4.8.9",
"textalk/websocket": "1.5.7"
},
Generated
+75 -74
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "66447142ffcb7c061df7a86729c8ecac",
"content-hash": "266a47c93a9de6f1c36211aa5cf52799",
"packages": [
{
"name": "adhocore/jwt",
@@ -115,11 +115,11 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.9.1",
"version": "0.10.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "01acf8741f539f64248d54a9f0f6c4d39195d16c"
"reference": "09874846c6bdb7be58c97b12323d2b35ec995409"
},
"require": {
"php": ">=8.0",
@@ -154,7 +154,7 @@
"php",
"runtimes"
],
"time": "2022-05-19T11:48:16+00:00"
"time": "2022-06-28T05:26:20+00:00"
},
{
"name": "chillerlan/php-qrcode",
@@ -1581,65 +1581,9 @@
},
"time": "2021-06-04T20:33:46+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.1",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-18T07:21:10+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@@ -1686,7 +1630,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1"
},
"funding": [
{
@@ -2107,16 +2051,16 @@
},
{
"name": "utopia-php/database",
"version": "0.17.1",
"version": "0.18.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "a4d001452b78b85335ffbd34176cd45d2b13c1ca"
"reference": "e9e163642546343267c2fe0ee90016a4a0230b4a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/a4d001452b78b85335ffbd34176cd45d2b13c1ca",
"reference": "a4d001452b78b85335ffbd34176cd45d2b13c1ca",
"url": "https://api.github.com/repos/utopia-php/database/zipball/e9e163642546343267c2fe0ee90016a4a0230b4a",
"reference": "e9e163642546343267c2fe0ee90016a4a0230b4a",
"shasum": ""
},
"require": {
@@ -2131,6 +2075,7 @@
"require-dev": {
"fakerphp/faker": "^1.14",
"phpunit/phpunit": "^9.4",
"swoole/ide-helper": "4.8.0",
"utopia-php/cli": "^0.11.0",
"vimeo/psalm": "4.0.1"
},
@@ -2164,9 +2109,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.17.1"
"source": "https://github.com/utopia-php/database/tree/0.18.6"
},
"time": "2022-05-16T09:11:44+00:00"
"time": "2022-06-27T17:28:05+00:00"
},
{
"name": "utopia-php/domains",
@@ -2949,16 +2894,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.18.8",
"version": "0.19.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "8ba45dfb74ff6062f96c0e4d10d7c4fae94768b1"
"reference": "51a8e4205cd4809deeff8121a24e717642d68564"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8ba45dfb74ff6062f96c0e4d10d7c4fae94768b1",
"reference": "8ba45dfb74ff6062f96c0e4d10d7c4fae94768b1",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/51a8e4205cd4809deeff8121a24e717642d68564",
"reference": "51a8e4205cd4809deeff8121a24e717642d68564",
"shasum": ""
},
"require": {
@@ -2993,9 +2938,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.18.8"
"source": "https://github.com/appwrite/sdk-generator/tree/0.19.4"
},
"time": "2022-05-19T10:34:06+00:00"
"time": "2022-06-29T15:19:58+00:00"
},
{
"name": "doctrine/instantiator",
@@ -5028,6 +4973,62 @@
],
"time": "2020-09-28T06:39:44+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.1",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-18T07:21:10+00:00"
},
{
"name": "swoole/ide-helper",
"version": "4.8.9",
+31 -3
View File
@@ -175,6 +175,8 @@ services:
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_GRAPHQL_MAX_COMPLEXITY
- _APP_GRAPHQL_MAX_DEPTH
- _APP_GRAPHQL_REQUEST_TIMEOUT
@@ -321,10 +323,10 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
appwrite-worker-database:
entrypoint: worker-database
appwrite-worker-databases:
entrypoint: worker-databases
<<: *x-logging
container_name: appwrite-worker-database
container_name: appwrite-worker-databases
build:
context: .
networks:
@@ -528,6 +530,30 @@ services:
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-worker-messaging:
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
build:
context: .
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-maintenance:
entrypoint: maintenance
<<: *x-logging
@@ -592,6 +618,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-schedule:
entrypoint: schedule
@@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createAnonymousSession(new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
});
}
}
@@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createEmailSession(
"email@example.com",
"password"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createJWT(new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
});
}
}
@@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createMagicURLSession(
"[USER_ID]",
"email@example.com",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createOAuth2Session(
this,
"amazon",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createPhoneSession(
"[USER_ID]",
""
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createPhoneVerification(new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
});
}
}
@@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createRecovery(
"email@example.com",
"https://example.com"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createVerification(
"https://example.com"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}
@@ -0,0 +1,50 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.create(
"[USER_ID]",
"email@example.com",
"password",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

Some files were not shown because too many files have changed in this diff Show More