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

# Conflicts:
#	CONTRIBUTING.md
#	app/config/errors.php
#	app/controllers/api/account.php
#	app/controllers/api/graphql.php
#	app/init.php
#	composer.json
#	composer.lock
This commit is contained in:
Jake Barnby
2022-06-20 11:15:22 +12:00
2115 changed files with 45900 additions and 37328 deletions
+14 -2
View File
@@ -26,12 +26,24 @@ _APP_DB_ROOT_PASS=rootsecretpassword
_APP_STORAGE_DEVICE=Local
_APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
_APP_STORAGE_S3_REGION=us-eas-1
_APP_STORAGE_S3_REGION=us-east-1
_APP_STORAGE_S3_BUCKET=
_APP_STORAGE_DO_SPACES_ACCESS_KEY=
_APP_STORAGE_DO_SPACES_SECRET=
_APP_STORAGE_DO_SPACES_REGION=us-eas-1
_APP_STORAGE_DO_SPACES_REGION=us-east-1
_APP_STORAGE_DO_SPACES_BUCKET=
_APP_STORAGE_BACKBLAZE_ACCESS_KEY=
_APP_STORAGE_BACKBLAZE_SECRET=
_APP_STORAGE_BACKBLAZE_REGION=us-west-004
_APP_STORAGE_BACKBLAZE_BUCKET=
_APP_STORAGE_LINODE_ACCESS_KEY=
_APP_STORAGE_LINODE_SECRET=
_APP_STORAGE_LINODE_REGION=eu-central-1
_APP_STORAGE_LINODE_BUCKET=
_APP_STORAGE_WASABI_ACCESS_KEY=
_APP_STORAGE_WASABI_SECRET=
_APP_STORAGE_WASABI_REGION=eu-central-1
_APP_STORAGE_WASABI_BUCKET=
_APP_STORAGE_ANTIVIRUS=disabled
_APP_STORAGE_ANTIVIRUS_HOST=clamav
_APP_STORAGE_ANTIVIRUS_PORT=3310
+1
View File
@@ -37,6 +37,7 @@ body:
label: "🎲 Appwrite version"
description: "What version of Appwrite are you running?"
options:
- Version 0.14.x
- Version 0.13.x
- Version 0.12.x
- Version 0.11.x
+34
View File
@@ -0,0 +1,34 @@
name: "Linter"
on: [pull_request]
jobs:
tests:
name: Linter
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
- name: Install dependencies
uses: php-actions/composer@v6
with:
php_version: '8.0'
args: --profile --ignore-platform-reqs
- name: Run Linter
run: ./vendor/bin/phpcs -p
-13
View File
@@ -1,13 +0,0 @@
FROM gitpod/workspace-full
# Disable current PHP installation
RUN sudo a2dismod php7.4
RUN sudo a2dismod mpm_prefork
# Install apache2 (PHP install requires to do this first)
RUN sudo apt --yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install apache2
# Update to PHP 8.0 with unattended installation
RUN sudo apt --yes install software-properties-common && sudo add-apt-repository ppa:ondrej/php -y
RUN sudo apt update
RUN sudo apt --yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install php8.0
+13 -6
View File
@@ -1,10 +1,17 @@
image:
file: .gitpod.Dockerfile
tasks:
- init: docker-compose pull &&
docker-compose build &&
docker run --rm --interactive --tty --volume $PWD:/app composer update --ignore-platform-reqs --optimize-autoloader --no-plugins --no-scripts --prefer-dist
- name: Run Appwrite Docker Stack
init: |
docker compose pull
docker compose build
command: |
docker run --rm --interactive --tty \
--volume $PWD:/app \
composer update \
--ignore-platform-reqs \
--optimize-autoloader \
--no-plugins \
--no-scripts \
--prefer-dist
ports:
- port: 8080
+85 -2
View File
@@ -1,5 +1,88 @@
# Unreleased Version
- Renamed `providers` to `authProviders` in project collection **Breaking Change**
# Version 0.14.2
## Features
- Support for Backblaze adapter in Storage
- Support for Linode adapter in Storage
- Support for Wasabi adapter in Storage
- New Cloud Function Runtimes:
- Dart 2.17
- Deno 1.21
- Java 18
- Node 18
- Improved overall Migration speed
# Version 0.14.1
## Bugs
* Fixed scheduled Cloud Functions execution with cron-job by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3245
* Fixed missing runtime icons by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3234
* Fixed Google OAuth by @Meldiron in https://github.com/appwrite/appwrite/pull/3236
* Fixed certificate generation when hostname was set to 'localhost' by @Meldiron in https://github.com/appwrite/appwrite/pull/3237
* Fixed Installation overriding default env variables by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3241
# Version 0.14.0
## Features
- **BREAKING CHANGE** New Event Model
- The new Event Model allows you to define events for Webhooks or Functions more granular
- Account and Users events have been merged to just Users
- Examples:
- `database.documents.create` is now `collections.[COLLECTION_ID].documents.[DOCUMENT_ID].create`
- Both placeholders needs to be replaced with either `*` for wildcard or an ID of the respective collection or document
- So you can listen to every document that is created in the `posts` collection with `collections.posts.*.documents.*.create`
- `event` in the Realtime payload has been renamed to `events` and contains all possible events
- `X-Appwrite-Webhook-Event` Webhook header has been renamed to `X-Appwrite-Webhook-Events` and contains all possible events
- **BREAKING CHANGE** Renamed `providers` to `authProviders` in Projects
- **BREAKING CHANGE** Renamed `stdout` to `response` in Execution
- **BREAKING CHANGE** Removed delete endpoint from the Accounts API
- **BREAKING CHANGE** Renamed `name` to `userName` on Membership response model
- **BREAKING CHANGE** Renamed `email` to `userEmail` on Membership response model
- **BREAKING CHANGE** Renamed `event` to `events` on Realtime Response and now is an array of strings
- Added `teamName` to Membership response model
- Added new endpoint to update user's status from the Accounts API
- Deleted users will now free their ID and not reserve it anymore
- Added new endpoint to list all memberships on the Users API
- Increased Execution `response` to 1MB
- Increased Build `stdout` to 1MB
- Added Wildcard support to Platforms
- Added Activity page to Teams console
- Added button to verify/unverify user's e-mail address in the console
- Added Docker log limits to `docker-compose.yaml`
- Renamed `_APP_EXECUTOR_RUNTIME_NETWORK` environment variable to `OPEN_RUNTIMES_NETWORK`
- Added Auth0 OAuth2 provider
- Added Okta Oauth2 provider @tanay1337 in https://github.com/appwrite/appwrite/pull/3139
## Bugs
- Fixed issues with `min`, `max` and `default` values for float attributes
- Fixed account created with Magic URL to set a new password
- Fixed Database to respect `null` values
- Fixed missing realtime events from the Users API
- Fixed missing events when all sessions are deleted from the Users and Account API
- Fixed dots in database attributes
- Fixed renewal of SSL certificates
- Fixed errors in the certificates workers
- Fixed HTTPS redirect bug for non GET requests
- Fixed search when a User is updated
- Fixed aspect ratio bug in Avatars API
- Fixed wrong `Fail to Warmup ...` error message in Executor
- Fixed UI when file uploader is covered by jumpt to top button
- Fixed bug that allowed Queries on failed indexes
- Fixed UI when an alert with a lot text disappears too fast by increasing duration
- Fixed issues with cache and case-sensivity on ID's
- Fixed storage stats by upgrading to `BIGINT`
- Fixed `storage.total` stats which now is a sum of `storage.files.total` and `storage.deployments.total`
- Fixed Project logo preview
- Fixed UI for missing icons in Collection attributes
- Fixed UI to allow single-character custom ID's
- Fixed array size validation in the Database Service
- Fixed file preview when file extension is missing
- Fixed `Open an Issue` link in the console
- Fixed missing environment variables on Executor service
- Fixed all endpoints that expect an Array in their params to have not more than 100 items
- Added Executor host variables as a part of infrastructure configuration by @sjke in https://github.com/appwrite/appwrite/pull/3084
- Added new tab/window for new release link by @Akshay-Rana-Gujjar in https://github.com/appwrite/appwrite/pull/3202
# Version 0.13.4
+101 -77
View File
@@ -12,11 +12,12 @@ Help us keep Appwrite open and inclusive. Please read and follow our [Code of Co
## Submit a Pull Request 🚀
Branch naming convention is as following
Branch naming convention is as following
`TYPE-ISSUE_ID-DESCRIPTION`
example:
```
doc-548-submit-a-pull-request-section-to-contribution-guide
```
@@ -29,32 +30,55 @@ When `TYPE` can be:
- **fix** - a bug fix
- **refactor** - code change that neither fixes a bug nor adds a feature
**All PRs must include a commit message with the changes description!**
**All PRs must include a commit message with the changes description!**
For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to:
1. `git pull`, before creating a new branch, pull the changes from upstream. Your master needs to be up to date.
```
$ git pull
```
2. Create new branch from `master` like: `doc-548-submit-a-pull-request-section-to-contribution-guide`<br/>
```
$ git checkout -b [name_of_your_new_branch]
```
3. Work - commit - repeat ( be sure to be in your branch )
4. Push changes to GitHub
4. Before you push your changes, make sure your code follows the `PSR12` coding standards , which is the standard Appwrite follows currently. You can easily do this by running the formatter.
```bash
./vendor/bin/phpcbf <your file path>
```
Now, go a step further by running the linter by the following command to manually fix the issues the formatter wasn't able to fix.
```bash
./vendor/bin/phpcs <your file path>
```
This will give you a list of errors for you to rectify , if there is an instance you need more information on the errors being displayed you can pass in additional command line arguments. More list of available arguments can be found [here](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). A very useful command line argument is `--report=diff`. This will give you the expected changes by the linter for easy fixing of formatting issues.
```bash
./vendor/bin/phpcs --report=diff <your file path>
```
5. Push changes to GitHub
```
$ git push origin [name_of_your_new_branch]
```
5. Submit your changes for review
If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
6. Start a Pull Request
Now submit the pull request and click on `Create pull request`.
7. Get a code review approval/reject
8. After approval, merge your PR
9. GitHub will automatically delete the branch after the merge is done. (they can still be restored).
6. Submit your changes for review
If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
7. Start a Pull Request
Now submit the pull request and click on `Create pull request`.
8. Get a code review approval/reject
9. After approval, merge your PR
10. GitHub will automatically delete the branch after the merge is done. (they can still be restored).
## Setup From Source
@@ -90,18 +114,20 @@ Appwrite uses an internal micro-framework called Litespeed.js to build simple UI
After finishing the installation process, you can start writing and editing code.
#### Advanced Topics
We love to create issues that are good for beginners and label them as `good first issue` or `hacktoberfest`, but some more advanced topics might require extra knowledge. Below is a list of links you can use to learn more about some of the more advance topics that will help you master the Appwrite codebase.
##### Tools and Libs
- [Docker](https://www.docker.com/get-started)
- [PHP FIG](https://www.php-fig.org/) - [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-4](https://www.php-fig.org/psr/psr-4/)
- [PHP FIG](https://www.php-fig.org/) - [PSR-12](https://www.php-fig.org/psr/psr-12/)
- [PHP Swoole](https://www.swoole.co.uk/)
Learn more at our [Technology Stack](#technology-stack) section.
##### Network and Protocols
- [OSI Model](https://en.wikipedia.org/wiki/OSI_model)
- [TCP vs UDP](https://www.guru99.com/tcp-vs-udp-understanding-the-difference.html#:~:text=TCP%20is%20a%20connection%2Doriented,speed%20of%20UDP%20is%20faster&text=TCP%20does%20error%20checking%20and,but%20it%20discards%20erroneous%20packets.)
- [HTTP](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)
@@ -110,10 +136,12 @@ Learn more at our [Technology Stack](#technology-stack) section.
- [gRPC](https://en.wikipedia.org/wiki/GRPC)
##### Architecture
- [Microservices vs Monolithic](https://www.mulesoft.com/resources/api/microservices-vs-monolithic#:~:text=Microservices%20architecture%20vs%20monolithic%20architecture&text=A%20monolithic%20application%20is%20built%20as%20a%20single%20unit.&text=To%20make%20any%20alterations%20to,formally%20with%20business%2Doriented%20APIs.)
- [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - Appwrite console architecture
##### Security
- [Appwrite Auth and ACL](https://github.com/appwrite/appwrite/blob/0.7.x/docs/specs/authentication.drawio.svg)
- [OAuth](https://en.wikipedia.org/wiki/OAuth)
- [Encryption](https://medium.com/searchencrypt/what-is-encryption-how-does-it-work-e8f20e340537#:~:text=Encryption%20is%20a%20process%20that,%2C%20or%20decrypt%2C%20the%20information.)
@@ -124,8 +152,8 @@ Learn more at our [Technology Stack](#technology-stack) section.
Appwrite's current structure is a combination of both [Monolithic](https://en.wikipedia.org/wiki/Monolithic_application) and [Microservice](https://en.wikipedia.org/wiki/Microservices) architectures, but our final goal, as we grow, is to be using only microservices.
---
![Appwrite](docs/specs/overview.drawio.svg)
---
## ![Appwrite](docs/specs/overview.drawio.svg)
### File Structure
@@ -204,15 +232,15 @@ Appwrite stack is combined from a variety of open-source technologies and tools.
### Other Technologies
* Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data)
* MariaDB - for database storage and queries
* InfluxDB - for managing stats and time-series based data
* Statsd - for sending data over UDP protocol (using Telegraf)
* ClamAV - for validating and scanning storage files
* Imagemagick - for manipulating and managing image media files.
* Webp - for better compression of images on supporting clients
* SMTP - for sending email messages and alerts
* Resque - for managing data queues and scheduled tasks over a Redis server
- Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data)
- MariaDB - for database storage and queries
- InfluxDB - for managing stats and time-series based data
- Statsd - for sending data over UDP protocol (using Telegraf)
- ClamAV - for validating and scanning storage files
- Imagemagick - for manipulating and managing image media files.
- Webp - for better compression of images on supporting clients
- SMTP - for sending email messages and alerts
- Resque - for managing data queues and scheduled tasks over a Redis server
## Package Managers
@@ -224,7 +252,7 @@ Appwrite uses [PHP's Composer](https://getcomposer.org/) for managing dependenci
## Coding Standards
Appwrite is following the [PHP-FIG standards](https://www.php-fig.org/). Currently, we are using both PSR-0 and PSR-4 for coding standards and autoloading standards. Soon we will also review the project for support with PSR-12 (Extended Coding Style).
Appwrite is following the [PHP-FIG standards](https://www.php-fig.org/). Currently, we are using both PSR-0 and PSR-12 for coding standards and autoloading standards.
We use prettier for our JS coding standards and auto-formatting our code.
@@ -236,14 +264,14 @@ We wish Appwrite will be as easy to set up and in a single, localhost, and easy
When contributing code, please take into account the following considerations:
* Response Time
* Throughput
* Requests per Seconds
* Network Usage
* Memory Usage
* Browser Rendering
* Background Jobs
* Task Execution Time
- Response Time
- Throughput
- Requests per Seconds
- Network Usage
- Memory Usage
- Browser Rendering
- Background Jobs
- Task Execution Time
## Security & Privacy
@@ -280,6 +308,7 @@ Before running the command, make sure you have proper write permissions to the A
```bash
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -t appwrite/appwrite:dev --push .
```
**Build Functions Runtimes**
The Runtimes for all supported cloud functions (multicore builds) can be found at the [open-runtimes/open-runtimes](https://github.com/open-runtimes/open-runtimes) repository.
@@ -299,11 +328,11 @@ For generating a new console SDK follow the next steps:
Things to remember when releasing SDKs
* Update the Changelogs in **docs/sdks** (right now only Dart and Flutter are using these)
* Update **GETTING_STARTED.md** in **docs/sdks** for each SDKs if any changes in the related APIs in there
* Update SDK versions as required on **app/config/platforms.php**
* Generate SDKs using the command `php app/cli.php sdks` and follow the instructions
* Release new tags on GitHub repository for each SDKs
- Update the Changelogs in **docs/sdks** (right now only Dart and Flutter are using these)
- Update **GETTING_STARTED.md** in **docs/sdks** for each SDKs if any changes in the related APIs in there
- Update SDK versions as required on **app/config/platforms.php**
- Generate SDKs using the command `php app/cli.php sdks` and follow the instructions
- Release new tags on GitHub repository for each SDKs
## Debug
@@ -315,13 +344,13 @@ First, you need to create an init file. Duplicate **dev/yasd_init.php.stub** fil
```json
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9005,
"pathMappings": {
"/usr/src/code": "${workspaceRoot}"
}
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9005,
"pathMappings": {
"/usr/src/code": "${workspaceRoot}"
}
}
```
@@ -354,67 +383,62 @@ To run end-2-end tests for a specific service use:
```bash
docker-compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName]
```
## Benchmarking
You can use WRK Docker image to benchmark the server performance. Benchmarking is extremely useful when you want to compare how the server behaves before and after a change has been applied. Replace [APPWRITE_HOSTNAME_OR_IP] with your Appwrite server hostname or IP. Note that localhost is not accessible from inside the WRK container.
```
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use
-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
--latency Print latency statistics
--timeout <T> Socket/request timeout
-v, --version Print version details
```
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use
-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
--latency Print latency statistics
--timeout <T> Socket/request timeout
-v, --version Print version details
```
```bash
docker run --rm skandyla/wrk -t3 -c100 -d30 https://[APPWRITE_HOSTNAME_OR_IP]
```
## Code Maintenance
## Code Maintenance
We use some automation tools to help us keep a healthy codebase.
Improve PHP execution time by using [fully-qualified function calls](https://veewee.github.io/blog/optimizing-php-performance-by-fq-function-calls/):
**Run Formatter:**
```bash
php-cs-fixer fix src/ --rules=native_function_invocation --allow-risky=yes
# Run on all files
./vendor/bin/phpcbf
# Run on single file or folder
./vendor/bin/phpcbf <your file path>
```
Coding Standards:
**Run Linter:**
```bash
php-cs-fixer fix app/controllers --rules='{"braces": {"allow_single_line_closure": true}}'
```
```bash
php-cs-fixer fix src --rules='{"braces": {"allow_single_line_closure": true}}'
```
Static Code Analysis:
```bash
docker-compose exec appwrite /usr/src/code/vendor/bin/psalm
# Run on all files
./vendor/bin/phpcs
# Run on single file or folder
./vendor/bin/phpcs <your file path>
```
## Tutorials
From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials:
* [Adding Support for a New OAuth2 Provider](./docs/tutorials/add-oauth2-provider.md)
* [Appwrite Environment Variables](./docs/tutorials/add-environment-variable.md)
* [Adding Support for a New Runtime](./docs/tutorials/add-runtime.md)
* [Adding Storage Adapter](./docs/tutorials/add-storage-adapter.md)
* [Adding Translations](./docs/tutorials/add-translations.md)
* [Multi-Architecture Support](./docs/tutorials/multi-architecture-support.md)
- [Adding Support for a New OAuth2 Provider](./docs/tutorials/add-oauth2-provider.md)
- [Appwrite Environment Variables](./docs/tutorials/environment-variables.md)
- [Running in Production](./docs/tutorials/running-in-production.md)
- [Adding Storage Adapter](./docs/tutorials/add-storage-adapter.md)
## Other Ways to Help
Pull requests are great, but there are many other areas where you can help Appwrite.
Pull requests are great, but there are many other areas where you can help Appwrite.
### Blogging & Speaking
+15 -3
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM node:16.13.2-alpine3.15 as node
FROM node:16.14.2-alpine3.15 as node
WORKDIR /usr/local/src/
@@ -30,8 +30,8 @@ ARG DEBUG=false
ENV DEBUG=$DEBUG
ENV PHP_REDIS_VERSION=5.3.7 \
PHP_MONGODB_VERSION=1.9.1 \
PHP_SWOOLE_VERSION=v4.8.7 \
PHP_MONGODB_VERSION=1.13.0 \
PHP_SWOOLE_VERSION=v4.8.9 \
PHP_IMAGICK_VERSION=3.7.0 \
PHP_YAML_VERSION=2.2.2 \
PHP_MAXMINDDB_VERSION=v1.11.0
@@ -162,6 +162,18 @@ ENV _APP_SERVER=swoole \
_APP_STORAGE_DO_SPACES_SECRET= \
_APP_STORAGE_DO_SPACES_REGION= \
_APP_STORAGE_DO_SPACES_BUCKET= \
_APP_STORAGE_BACKBLAZE_ACCESS_KEY= \
_APP_STORAGE_BACKBLAZE_SECRET= \
_APP_STORAGE_BACKBLAZE_REGION= \
_APP_STORAGE_BACKBLAZE_BUCKET= \
_APP_STORAGE_LINODE_ACCESS_KEY= \
_APP_STORAGE_LINODE_SECRET= \
_APP_STORAGE_LINODE_REGION= \
_APP_STORAGE_LINODE_BUCKET= \
_APP_STORAGE_WASABI_ACCESS_KEY= \
_APP_STORAGE_WASABI_SECRET= \
_APP_STORAGE_WASABI_REGION= \
_APP_STORAGE_WASABI_BUCKET= \
_APP_REDIS_HOST=redis \
_APP_REDIS_PORT=6379 \
_APP_DB_HOST=mariadb \
+3 -3
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.13.4
appwrite/appwrite:0.14.2
```
### 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.13.4
appwrite/appwrite:0.14.2
```
#### PowerShell
@@ -81,7 +81,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.13.4
appwrite/appwrite:0.14.2
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+4 -4
View File
@@ -19,7 +19,7 @@
English | [简体中文](README-CN.md)
[**Appwrite 0.13 has been released! Learn what's new!**](https://dev.to/appwrite/announcing-appwrite-013-with-major-upgrades-to-storage-and-functions-3hpf)
[**Appwrite 0.14 has been released! Learn what's new!**](https://dev.to/appwrite/announcing-appwrite-014-with-11-cloud-function-runtimes-36f5)
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 +62,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.13.4
appwrite/appwrite:0.14.2
```
### Windows
@@ -74,7 +74,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.13.4
appwrite/appwrite:0.14.2
```
#### PowerShell
@@ -84,7 +84,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.13.4
appwrite/appwrite:0.14.2
```
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.
+7 -3
View File
@@ -1,10 +1,14 @@
<?php
require_once __DIR__.'/init.php';
require_once __DIR__.'/controllers/general.php';
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/controllers/general.php';
use Utopia\App;
use Utopia\CLI\CLI;
use Utopia\CLI\Console;
use Utopia\Database\Validator\Authorization;
Authorization::disable();
$cli = new CLI();
@@ -25,4 +29,4 @@ $cli
Console::log(App::getEnv('_APP_VERSION', 'UNKNOWN'));
});
$cli->run();
$cli->run();
+2 -2
View File
@@ -1,4 +1,4 @@
<?php
<?php
// Auth methods
@@ -45,4 +45,4 @@ return [
'docs' => '',
'enabled' => false,
],
];
];
+14 -14
View File
@@ -2,20 +2,20 @@
return [
// Codes based on: https://github.com/matomo-org/device-detector/blob/master/Parser/Client/Browser.php
'aa' => __DIR__.'/browsers/avant.png',
'an' => __DIR__.'/browsers/android-webview-beta.png',
'ch' => __DIR__.'/browsers/chrome.png',
'ci' => __DIR__.'/browsers/chrome.png', //Chrome Mobile iOS
'cm' => __DIR__.'/browsers/chrome.png', //Chrome Mobile
'cr' => __DIR__.'/browsers/chromium.png',
'ff' => __DIR__.'/browsers/firefox.png',
'sf' => __DIR__.'/browsers/safari.png',
'mf' => __DIR__.'/browsers/safari.png',
'ps' => __DIR__.'/browsers/edge.png',
'oi' => __DIR__.'/browsers/edge.png',
'om' => __DIR__.'/browsers/opera-mini.png',
'op' => __DIR__.'/browsers/opera.png',
'on' => __DIR__.'/browsers/opera.png',
'aa' => __DIR__ . '/browsers/avant.png',
'an' => __DIR__ . '/browsers/android-webview-beta.png',
'ch' => __DIR__ . '/browsers/chrome.png',
'ci' => __DIR__ . '/browsers/chrome.png', //Chrome Mobile iOS
'cm' => __DIR__ . '/browsers/chrome.png', //Chrome Mobile
'cr' => __DIR__ . '/browsers/chromium.png',
'ff' => __DIR__ . '/browsers/firefox.png',
'sf' => __DIR__ . '/browsers/safari.png',
'mf' => __DIR__ . '/browsers/safari.png',
'ps' => __DIR__ . '/browsers/edge.png',
'oi' => __DIR__ . '/browsers/edge.png',
'om' => __DIR__ . '/browsers/opera-mini.png',
'op' => __DIR__ . '/browsers/opera.png',
'on' => __DIR__ . '/browsers/opera.png',
/*
'36' => '360 Phone Browser',
+16 -16
View File
@@ -1,20 +1,20 @@
<?php
return [
'amex' => __DIR__.'/credit-cards/amex.png',
'argencard' => __DIR__.'/credit-cards/argencard.png',
'cabal' => __DIR__.'/credit-cards/cabal.png',
'censosud' => __DIR__.'/credit-cards/consosud.png',
'diners' => __DIR__.'/credit-cards/diners.png',
'discover' => __DIR__.'/credit-cards/discover.png',
'elo' => __DIR__.'/credit-cards/elo.png',
'hipercard' => __DIR__.'/credit-cards/hipercard.png',
'jcb' => __DIR__.'/credit-cards/jcb.png',
'mastercard' => __DIR__.'/credit-cards/mastercard.png',
'naranja' => __DIR__.'/credit-cards/naranja.png',
'targeta-shopping' => __DIR__.'/credit-cards/tarjeta-shopping.png',
'union-china-pay' => __DIR__.'/credit-cards/union-china-pay.png',
'visa' => __DIR__.'/credit-cards/visa.png',
'mir' => __DIR__.'/credit-cards/mir.png',
'maestro' => __DIR__.'/credit-cards/maestro.png',
'amex' => __DIR__ . '/credit-cards/amex.png',
'argencard' => __DIR__ . '/credit-cards/argencard.png',
'cabal' => __DIR__ . '/credit-cards/cabal.png',
'censosud' => __DIR__ . '/credit-cards/consosud.png',
'diners' => __DIR__ . '/credit-cards/diners.png',
'discover' => __DIR__ . '/credit-cards/discover.png',
'elo' => __DIR__ . '/credit-cards/elo.png',
'hipercard' => __DIR__ . '/credit-cards/hipercard.png',
'jcb' => __DIR__ . '/credit-cards/jcb.png',
'mastercard' => __DIR__ . '/credit-cards/mastercard.png',
'naranja' => __DIR__ . '/credit-cards/naranja.png',
'targeta-shopping' => __DIR__ . '/credit-cards/tarjeta-shopping.png',
'union-china-pay' => __DIR__ . '/credit-cards/union-china-pay.png',
'visa' => __DIR__ . '/credit-cards/visa.png',
'mir' => __DIR__ . '/credit-cards/mir.png',
'maestro' => __DIR__ . '/credit-cards/maestro.png',
];
+194 -194
View File
@@ -1,198 +1,198 @@
<?php
return [
'af' => __DIR__.'/flags/af.png',
'ao' => __DIR__.'/flags/ao.png',
'al' => __DIR__.'/flags/al.png',
'ad' => __DIR__.'/flags/ad.png',
'ae' => __DIR__.'/flags/ae.png',
'ar' => __DIR__.'/flags/ar.png',
'am' => __DIR__.'/flags/am.png',
'ag' => __DIR__.'/flags/ag.png',
'au' => __DIR__.'/flags/au.png',
'at' => __DIR__.'/flags/at.png',
'az' => __DIR__.'/flags/az.png',
'bi' => __DIR__.'/flags/bi.png',
'be' => __DIR__.'/flags/be.png',
'bj' => __DIR__.'/flags/bj.png',
'bf' => __DIR__.'/flags/bf.png',
'bd' => __DIR__.'/flags/bd.png',
'bg' => __DIR__.'/flags/bg.png',
'bh' => __DIR__.'/flags/bh.png',
'bs' => __DIR__.'/flags/bs.png',
'ba' => __DIR__.'/flags/ba.png',
'by' => __DIR__.'/flags/by.png',
'bz' => __DIR__.'/flags/bz.png',
'bo' => __DIR__.'/flags/bo.png',
'br' => __DIR__.'/flags/br.png',
'bb' => __DIR__.'/flags/bb.png',
'bn' => __DIR__.'/flags/bn.png',
'bt' => __DIR__.'/flags/bt.png',
'bw' => __DIR__.'/flags/bw.png',
'cf' => __DIR__.'/flags/cf.png',
'ca' => __DIR__.'/flags/ca.png',
'ch' => __DIR__.'/flags/ch.png',
'cl' => __DIR__.'/flags/cl.png',
'cn' => __DIR__.'/flags/cn.png',
'ci' => __DIR__.'/flags/ci.png',
'cm' => __DIR__.'/flags/cm.png',
'cd' => __DIR__.'/flags/cd.png',
'cg' => __DIR__.'/flags/cg.png',
'co' => __DIR__.'/flags/co.png',
'km' => __DIR__.'/flags/km.png',
'cv' => __DIR__.'/flags/cv.png',
'cr' => __DIR__.'/flags/cr.png',
'cu' => __DIR__.'/flags/cu.png',
'cy' => __DIR__.'/flags/cy.png',
'cz' => __DIR__.'/flags/cz.png',
'de' => __DIR__.'/flags/de.png',
'dj' => __DIR__.'/flags/dj.png',
'dm' => __DIR__.'/flags/dm.png',
'dk' => __DIR__.'/flags/dk.png',
'do' => __DIR__.'/flags/do.png',
'dz' => __DIR__.'/flags/dz.png',
'ec' => __DIR__.'/flags/ec.png',
'eg' => __DIR__.'/flags/eg.png',
'er' => __DIR__.'/flags/er.png',
'es' => __DIR__.'/flags/es.png',
'ee' => __DIR__.'/flags/ee.png',
'et' => __DIR__.'/flags/et.png',
'fi' => __DIR__.'/flags/fi.png',
'fj' => __DIR__.'/flags/fj.png',
'fr' => __DIR__.'/flags/fr.png',
'fm' => __DIR__.'/flags/fm.png',
'ga' => __DIR__.'/flags/ga.png',
'gb' => __DIR__.'/flags/gb.png',
'ge' => __DIR__.'/flags/ge.png',
'gh' => __DIR__.'/flags/gh.png',
'gn' => __DIR__.'/flags/gn.png',
'gm' => __DIR__.'/flags/gm.png',
'gw' => __DIR__.'/flags/gw.png',
'gq' => __DIR__.'/flags/gq.png',
'gr' => __DIR__.'/flags/gr.png',
'gd' => __DIR__.'/flags/gd.png',
'gt' => __DIR__.'/flags/gt.png',
'gy' => __DIR__.'/flags/gy.png',
'hn' => __DIR__.'/flags/hn.png',
'hr' => __DIR__.'/flags/hr.png',
'ht' => __DIR__.'/flags/ht.png',
'hu' => __DIR__.'/flags/hu.png',
'id' => __DIR__.'/flags/id.png',
'in' => __DIR__.'/flags/in.png',
'ie' => __DIR__.'/flags/ie.png',
'ir' => __DIR__.'/flags/ir.png',
'iq' => __DIR__.'/flags/iq.png',
'is' => __DIR__.'/flags/is.png',
'il' => __DIR__.'/flags/il.png',
'it' => __DIR__.'/flags/it.png',
'jm' => __DIR__.'/flags/jm.png',
'jo' => __DIR__.'/flags/jo.png',
'jp' => __DIR__.'/flags/jp.png',
'kz' => __DIR__.'/flags/kz.png',
'ke' => __DIR__.'/flags/ke.png',
'kg' => __DIR__.'/flags/kg.png',
'kh' => __DIR__.'/flags/kh.png',
'ki' => __DIR__.'/flags/ki.png',
'kn' => __DIR__.'/flags/kn.png',
'kr' => __DIR__.'/flags/kr.png',
'kw' => __DIR__.'/flags/kw.png',
'la' => __DIR__.'/flags/la.png',
'lb' => __DIR__.'/flags/lb.png',
'lr' => __DIR__.'/flags/lr.png',
'ly' => __DIR__.'/flags/ly.png',
'lc' => __DIR__.'/flags/lc.png',
'li' => __DIR__.'/flags/li.png',
'lk' => __DIR__.'/flags/lk.png',
'ls' => __DIR__.'/flags/ls.png',
'lt' => __DIR__.'/flags/ls.png',
'lu' => __DIR__.'/flags/lu.png',
'lv' => __DIR__.'/flags/lv.png',
'ma' => __DIR__.'/flags/ma.png',
'mc' => __DIR__.'/flags/mc.png',
'md' => __DIR__.'/flags/md.png',
'mg' => __DIR__.'/flags/mg.png',
'mv' => __DIR__.'/flags/mv.png',
'mx' => __DIR__.'/flags/mx.png',
'mh' => __DIR__.'/flags/mh.png',
'mk' => __DIR__.'/flags/mk.png',
'ml' => __DIR__.'/flags/ml.png',
'mt' => __DIR__.'/flags/mt.png',
'mm' => __DIR__.'/flags/mm.png',
'me' => __DIR__.'/flags/me.png',
'mn' => __DIR__.'/flags/mn.png',
'mz' => __DIR__.'/flags/mz.png',
'mr' => __DIR__.'/flags/mr.png',
'mu' => __DIR__.'/flags/mu.png',
'mw' => __DIR__.'/flags/mw.png',
'my' => __DIR__.'/flags/my.png',
'na' => __DIR__.'/flags/na.png',
'ne' => __DIR__.'/flags/ne.png',
'ng' => __DIR__.'/flags/ng.png',
'ni' => __DIR__.'/flags/ni.png',
'nl' => __DIR__.'/flags/nl.png',
'no' => __DIR__.'/flags/no.png',
'np' => __DIR__.'/flags/np.png',
'nr' => __DIR__.'/flags/nr.png',
'nz' => __DIR__.'/flags/nz.png',
'om' => __DIR__.'/flags/om.png',
'pk' => __DIR__.'/flags/pk.png',
'pa' => __DIR__.'/flags/pa.png',
'pe' => __DIR__.'/flags/pe.png',
'ph' => __DIR__.'/flags/ph.png',
'pw' => __DIR__.'/flags/pw.png',
'pg' => __DIR__.'/flags/pg.png',
'pl' => __DIR__.'/flags/pl.png',
'kp' => __DIR__.'/flags/kp.png',
'pt' => __DIR__.'/flags/pt.png',
'py' => __DIR__.'/flags/py.png',
'qa' => __DIR__.'/flags/qa.png',
'ro' => __DIR__.'/flags/ro.png',
'ru' => __DIR__.'/flags/ru.png',
'rw' => __DIR__.'/flags/rw.png',
'sa' => __DIR__.'/flags/sa.png',
'sd' => __DIR__.'/flags/sd.png',
'sn' => __DIR__.'/flags/sn.png',
'sg' => __DIR__.'/flags/sg.png',
'sb' => __DIR__.'/flags/sb.png',
'sl' => __DIR__.'/flags/sl.png',
'sv' => __DIR__.'/flags/sv.png',
'sm' => __DIR__.'/flags/sm.png',
'so' => __DIR__.'/flags/so.png',
'rs' => __DIR__.'/flags/rs.png',
'ss' => __DIR__.'/flags/ss.png',
'st' => __DIR__.'/flags/st.png',
'sr' => __DIR__.'/flags/sr.png',
'sk' => __DIR__.'/flags/sk.png',
'si' => __DIR__.'/flags/si.png',
'se' => __DIR__.'/flags/se.png',
'sz' => __DIR__.'/flags/sz.png',
'sc' => __DIR__.'/flags/sc.png',
'sy' => __DIR__.'/flags/sy.png',
'td' => __DIR__.'/flags/td.png',
'tg' => __DIR__.'/flags/tg.png',
'th' => __DIR__.'/flags/th.png',
'tj' => __DIR__.'/flags/tj.png',
'tm' => __DIR__.'/flags/tm.png',
'tl' => __DIR__.'/flags/tl.png',
'to' => __DIR__.'/flags/to.png',
'tt' => __DIR__.'/flags/tt.png',
'tn' => __DIR__.'/flags/tn.png',
'tr' => __DIR__.'/flags/tr.png',
'tv' => __DIR__.'/flags/tv.png',
'tz' => __DIR__.'/flags/tz.png',
'ug' => __DIR__.'/flags/ug.png',
'ua' => __DIR__.'/flags/ua.png',
'uy' => __DIR__.'/flags/uy.png',
'us' => __DIR__.'/flags/us.png',
'uz' => __DIR__.'/flags/uz.png',
'va' => __DIR__.'/flags/va.png',
'vc' => __DIR__.'/flags/vc.png',
've' => __DIR__.'/flags/ve.png',
'vn' => __DIR__.'/flags/vn.png',
'vu' => __DIR__.'/flags/vu.png',
'ws' => __DIR__.'/flags/ws.png',
'ye' => __DIR__.'/flags/ye.png',
'za' => __DIR__.'/flags/za.png',
'zm' => __DIR__.'/flags/zm.png',
'zw' => __DIR__.'/flags/zw.png',
'af' => __DIR__ . '/flags/af.png',
'ao' => __DIR__ . '/flags/ao.png',
'al' => __DIR__ . '/flags/al.png',
'ad' => __DIR__ . '/flags/ad.png',
'ae' => __DIR__ . '/flags/ae.png',
'ar' => __DIR__ . '/flags/ar.png',
'am' => __DIR__ . '/flags/am.png',
'ag' => __DIR__ . '/flags/ag.png',
'au' => __DIR__ . '/flags/au.png',
'at' => __DIR__ . '/flags/at.png',
'az' => __DIR__ . '/flags/az.png',
'bi' => __DIR__ . '/flags/bi.png',
'be' => __DIR__ . '/flags/be.png',
'bj' => __DIR__ . '/flags/bj.png',
'bf' => __DIR__ . '/flags/bf.png',
'bd' => __DIR__ . '/flags/bd.png',
'bg' => __DIR__ . '/flags/bg.png',
'bh' => __DIR__ . '/flags/bh.png',
'bs' => __DIR__ . '/flags/bs.png',
'ba' => __DIR__ . '/flags/ba.png',
'by' => __DIR__ . '/flags/by.png',
'bz' => __DIR__ . '/flags/bz.png',
'bo' => __DIR__ . '/flags/bo.png',
'br' => __DIR__ . '/flags/br.png',
'bb' => __DIR__ . '/flags/bb.png',
'bn' => __DIR__ . '/flags/bn.png',
'bt' => __DIR__ . '/flags/bt.png',
'bw' => __DIR__ . '/flags/bw.png',
'cf' => __DIR__ . '/flags/cf.png',
'ca' => __DIR__ . '/flags/ca.png',
'ch' => __DIR__ . '/flags/ch.png',
'cl' => __DIR__ . '/flags/cl.png',
'cn' => __DIR__ . '/flags/cn.png',
'ci' => __DIR__ . '/flags/ci.png',
'cm' => __DIR__ . '/flags/cm.png',
'cd' => __DIR__ . '/flags/cd.png',
'cg' => __DIR__ . '/flags/cg.png',
'co' => __DIR__ . '/flags/co.png',
'km' => __DIR__ . '/flags/km.png',
'cv' => __DIR__ . '/flags/cv.png',
'cr' => __DIR__ . '/flags/cr.png',
'cu' => __DIR__ . '/flags/cu.png',
'cy' => __DIR__ . '/flags/cy.png',
'cz' => __DIR__ . '/flags/cz.png',
'de' => __DIR__ . '/flags/de.png',
'dj' => __DIR__ . '/flags/dj.png',
'dm' => __DIR__ . '/flags/dm.png',
'dk' => __DIR__ . '/flags/dk.png',
'do' => __DIR__ . '/flags/do.png',
'dz' => __DIR__ . '/flags/dz.png',
'ec' => __DIR__ . '/flags/ec.png',
'eg' => __DIR__ . '/flags/eg.png',
'er' => __DIR__ . '/flags/er.png',
'es' => __DIR__ . '/flags/es.png',
'ee' => __DIR__ . '/flags/ee.png',
'et' => __DIR__ . '/flags/et.png',
'fi' => __DIR__ . '/flags/fi.png',
'fj' => __DIR__ . '/flags/fj.png',
'fr' => __DIR__ . '/flags/fr.png',
'fm' => __DIR__ . '/flags/fm.png',
'ga' => __DIR__ . '/flags/ga.png',
'gb' => __DIR__ . '/flags/gb.png',
'ge' => __DIR__ . '/flags/ge.png',
'gh' => __DIR__ . '/flags/gh.png',
'gn' => __DIR__ . '/flags/gn.png',
'gm' => __DIR__ . '/flags/gm.png',
'gw' => __DIR__ . '/flags/gw.png',
'gq' => __DIR__ . '/flags/gq.png',
'gr' => __DIR__ . '/flags/gr.png',
'gd' => __DIR__ . '/flags/gd.png',
'gt' => __DIR__ . '/flags/gt.png',
'gy' => __DIR__ . '/flags/gy.png',
'hn' => __DIR__ . '/flags/hn.png',
'hr' => __DIR__ . '/flags/hr.png',
'ht' => __DIR__ . '/flags/ht.png',
'hu' => __DIR__ . '/flags/hu.png',
'id' => __DIR__ . '/flags/id.png',
'in' => __DIR__ . '/flags/in.png',
'ie' => __DIR__ . '/flags/ie.png',
'ir' => __DIR__ . '/flags/ir.png',
'iq' => __DIR__ . '/flags/iq.png',
'is' => __DIR__ . '/flags/is.png',
'il' => __DIR__ . '/flags/il.png',
'it' => __DIR__ . '/flags/it.png',
'jm' => __DIR__ . '/flags/jm.png',
'jo' => __DIR__ . '/flags/jo.png',
'jp' => __DIR__ . '/flags/jp.png',
'kz' => __DIR__ . '/flags/kz.png',
'ke' => __DIR__ . '/flags/ke.png',
'kg' => __DIR__ . '/flags/kg.png',
'kh' => __DIR__ . '/flags/kh.png',
'ki' => __DIR__ . '/flags/ki.png',
'kn' => __DIR__ . '/flags/kn.png',
'kr' => __DIR__ . '/flags/kr.png',
'kw' => __DIR__ . '/flags/kw.png',
'la' => __DIR__ . '/flags/la.png',
'lb' => __DIR__ . '/flags/lb.png',
'lr' => __DIR__ . '/flags/lr.png',
'ly' => __DIR__ . '/flags/ly.png',
'lc' => __DIR__ . '/flags/lc.png',
'li' => __DIR__ . '/flags/li.png',
'lk' => __DIR__ . '/flags/lk.png',
'ls' => __DIR__ . '/flags/ls.png',
'lt' => __DIR__ . '/flags/ls.png',
'lu' => __DIR__ . '/flags/lu.png',
'lv' => __DIR__ . '/flags/lv.png',
'ma' => __DIR__ . '/flags/ma.png',
'mc' => __DIR__ . '/flags/mc.png',
'md' => __DIR__ . '/flags/md.png',
'mg' => __DIR__ . '/flags/mg.png',
'mv' => __DIR__ . '/flags/mv.png',
'mx' => __DIR__ . '/flags/mx.png',
'mh' => __DIR__ . '/flags/mh.png',
'mk' => __DIR__ . '/flags/mk.png',
'ml' => __DIR__ . '/flags/ml.png',
'mt' => __DIR__ . '/flags/mt.png',
'mm' => __DIR__ . '/flags/mm.png',
'me' => __DIR__ . '/flags/me.png',
'mn' => __DIR__ . '/flags/mn.png',
'mz' => __DIR__ . '/flags/mz.png',
'mr' => __DIR__ . '/flags/mr.png',
'mu' => __DIR__ . '/flags/mu.png',
'mw' => __DIR__ . '/flags/mw.png',
'my' => __DIR__ . '/flags/my.png',
'na' => __DIR__ . '/flags/na.png',
'ne' => __DIR__ . '/flags/ne.png',
'ng' => __DIR__ . '/flags/ng.png',
'ni' => __DIR__ . '/flags/ni.png',
'nl' => __DIR__ . '/flags/nl.png',
'no' => __DIR__ . '/flags/no.png',
'np' => __DIR__ . '/flags/np.png',
'nr' => __DIR__ . '/flags/nr.png',
'nz' => __DIR__ . '/flags/nz.png',
'om' => __DIR__ . '/flags/om.png',
'pk' => __DIR__ . '/flags/pk.png',
'pa' => __DIR__ . '/flags/pa.png',
'pe' => __DIR__ . '/flags/pe.png',
'ph' => __DIR__ . '/flags/ph.png',
'pw' => __DIR__ . '/flags/pw.png',
'pg' => __DIR__ . '/flags/pg.png',
'pl' => __DIR__ . '/flags/pl.png',
'kp' => __DIR__ . '/flags/kp.png',
'pt' => __DIR__ . '/flags/pt.png',
'py' => __DIR__ . '/flags/py.png',
'qa' => __DIR__ . '/flags/qa.png',
'ro' => __DIR__ . '/flags/ro.png',
'ru' => __DIR__ . '/flags/ru.png',
'rw' => __DIR__ . '/flags/rw.png',
'sa' => __DIR__ . '/flags/sa.png',
'sd' => __DIR__ . '/flags/sd.png',
'sn' => __DIR__ . '/flags/sn.png',
'sg' => __DIR__ . '/flags/sg.png',
'sb' => __DIR__ . '/flags/sb.png',
'sl' => __DIR__ . '/flags/sl.png',
'sv' => __DIR__ . '/flags/sv.png',
'sm' => __DIR__ . '/flags/sm.png',
'so' => __DIR__ . '/flags/so.png',
'rs' => __DIR__ . '/flags/rs.png',
'ss' => __DIR__ . '/flags/ss.png',
'st' => __DIR__ . '/flags/st.png',
'sr' => __DIR__ . '/flags/sr.png',
'sk' => __DIR__ . '/flags/sk.png',
'si' => __DIR__ . '/flags/si.png',
'se' => __DIR__ . '/flags/se.png',
'sz' => __DIR__ . '/flags/sz.png',
'sc' => __DIR__ . '/flags/sc.png',
'sy' => __DIR__ . '/flags/sy.png',
'td' => __DIR__ . '/flags/td.png',
'tg' => __DIR__ . '/flags/tg.png',
'th' => __DIR__ . '/flags/th.png',
'tj' => __DIR__ . '/flags/tj.png',
'tm' => __DIR__ . '/flags/tm.png',
'tl' => __DIR__ . '/flags/tl.png',
'to' => __DIR__ . '/flags/to.png',
'tt' => __DIR__ . '/flags/tt.png',
'tn' => __DIR__ . '/flags/tn.png',
'tr' => __DIR__ . '/flags/tr.png',
'tv' => __DIR__ . '/flags/tv.png',
'tz' => __DIR__ . '/flags/tz.png',
'ug' => __DIR__ . '/flags/ug.png',
'ua' => __DIR__ . '/flags/ua.png',
'uy' => __DIR__ . '/flags/uy.png',
'us' => __DIR__ . '/flags/us.png',
'uz' => __DIR__ . '/flags/uz.png',
'va' => __DIR__ . '/flags/va.png',
'vc' => __DIR__ . '/flags/vc.png',
've' => __DIR__ . '/flags/ve.png',
'vn' => __DIR__ . '/flags/vn.png',
'vu' => __DIR__ . '/flags/vu.png',
'ws' => __DIR__ . '/flags/ws.png',
'ye' => __DIR__ . '/flags/ye.png',
'za' => __DIR__ . '/flags/za.png',
'zm' => __DIR__ . '/flags/zm.png',
'zw' => __DIR__ . '/flags/zw.png',
];
+3 -3
View File
@@ -2,9 +2,9 @@
return [
// Codes based on: https://github.com/matomo-org/device-detector/blob/master/Parser/Client/Browser.php
'AND' => __DIR__.'/os/android.png',
'ATV' => __DIR__.'/os/apple-tv.png',
'COS' => __DIR__.'/os/chrome-os.png',
'AND' => __DIR__ . '/os/android.png',
'ATV' => __DIR__ . '/os/apple-tv.png',
'COS' => __DIR__ . '/os/chrome-os.png',
/*
'AIX' => 'AIX',
+118 -35
View File
@@ -232,7 +232,7 @@ $collections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => new stdClass,
'default' => new stdClass(),
'array' => false,
'filters' => ['json', 'range', 'enum'],
],
@@ -931,6 +931,17 @@ $collections = [
'array' => true,
'filters' => [],
],
[
'$id' => 'signatureKey',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@@ -1054,9 +1065,9 @@ $collections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => ['json'],
'default' => null,
'array' => false,
'filters' => ['subQuerySessions'],
],
[
'$id' => 'tokens',
@@ -1065,9 +1076,9 @@ $collections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => ['json'],
'default' => null,
'array' => false,
'filters' => ['subQueryTokens'],
],
[
'$id' => 'memberships',
@@ -1076,9 +1087,9 @@ $collections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => ['json'],
'default' => null,
'array' => false,
'filters' => ['subQueryMemberships'],
],
[
'$id' => 'search',
@@ -1090,18 +1101,7 @@ $collections = [
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'deleted',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
]
],
'indexes' => [
[
@@ -1117,13 +1117,89 @@ $collections = [
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
],
'tokens' => [
'$collection' => Database::METADATA,
'$id' => 'tokens',
'name' => 'Tokens',
'attributes' => [
[
'$id' => 'userId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => '_key_deleted_email',
'$id' => 'type',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 512, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql (512 for encryption)
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'expire',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'userAgent',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'ip',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 45, // https://stackoverflow.com/a/166157/2299554
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
'$id' => '_key_user',
'type' => Database::INDEX_KEY,
'attributes' => ['deleted', 'email'],
'lengths' => [0, 320],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -1395,6 +1471,13 @@ $collections = [
'lengths' => [100, 100],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
[
'$id' => '_key_user',
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -1983,7 +2066,7 @@ $collections = [
],
[
'$id' => 'status',
'type' => Database::VAR_STRING,
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
@@ -2007,7 +2090,7 @@ $collections = [
'$id' => 'stderr',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => '',
@@ -2018,7 +2101,7 @@ $collections = [
'$id' => 'stdout',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => '',
@@ -2027,7 +2110,7 @@ $collections = [
],
[
'$id' => 'sourceType',
'type' => Database::VAR_STRING,
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
@@ -2038,7 +2121,7 @@ $collections = [
],
[
'$id' => 'source',
'type' => Database::VAR_STRING,
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
@@ -2121,10 +2204,10 @@ $collections = [
'filters' => [],
],
[
'$id' => 'stdout',
'$id' => 'response',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => null,
@@ -2135,7 +2218,7 @@ $collections = [
'$id' => 'stderr',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => null,
@@ -2425,7 +2508,7 @@ $collections = [
'$id' => 'value',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'size' => 8,
'signed' => false,
'required' => true,
'default' => null,
+11 -5
View File
@@ -1,7 +1,7 @@
<?php
/**
* List of server wide error codes and their respective messages.
* List of server wide error codes and their respective messages.
*/
use Appwrite\Extend\Exception;
@@ -11,17 +11,17 @@ return [
Exception::GENERAL_UNKNOWN => [
'name' => Exception::GENERAL_UNKNOWN,
'description' => 'An unknown error has occured. Please check the logs for more information.',
'code' => 500,
'code' => 500,
],
Exception::GENERAL_MOCK => [
'name' => Exception::GENERAL_MOCK,
'description' => 'General errors thrown by the mock controller used for testing.',
'code' => 400,
'code' => 400,
],
Exception::GENERAL_ACCESS_FORBIDDEN => [
'name' => Exception::GENERAL_ACCESS_FORBIDDEN,
'description' => 'Access to this API is forbidden.',
'code' => 401,
'code' => 401,
],
Exception::GENERAL_UNKNOWN_ORIGIN => [
'name' => Exception::GENERAL_UNKNOWN_ORIGIN,
@@ -78,6 +78,11 @@ return [
'description' => 'An internal server error occurred.',
'code' => 500,
],
Exception::GENERAL_PROTOCOL_UNSUPPORTED => [
'name' => Exception::GENERAL_PROTOCOL_UNSUPPORTED,
'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.',
'code' => 500,
],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [
@@ -497,4 +502,5 @@ return [
'description' => 'Query is required and can be provided via parameter or as the raw body if the content-type header is application/graphql.',
'code' => 400,
],
];
];
];
+209 -271
View File
@@ -7,279 +7,217 @@
use Appwrite\Utopia\Response;
return [
'account.create' => [
'description' => 'This event triggers when the account is created.',
'model' => Response::MODEL_USER,
'note' => '',
'users' => [
'$model' => Response::MODEL_USER,
'$resource' => true,
'$description' => 'This event triggers on any user\'s event.',
'sessions' => [
'$model' => Response::MODEL_SESSION,
'$resource' => true,
'$description' => 'This event triggers on any user\'s sessions event.',
'create' => [
'$description' => 'This event triggers when a session for a user is created.',
],
'delete' => [
'$description' => 'This event triggers when a session for a user is deleted.'
],
],
'recovery' => [
'$model' => Response::MODEL_TOKEN,
'$resource' => true,
'$description' => 'This event triggers on any user\'s recovery token event.',
'create' => [
'$description' => 'This event triggers when a recovery token for a user is created.',
],
'update' => [
'$description' => 'This event triggers when a recovery token for a user is validated.'
],
],
'verification' => [
'$model' => Response::MODEL_TOKEN,
'$resource' => true,
'$description' => 'This event triggers on any user\'s verification token event.',
'create' => [
'$description' => 'This event triggers when a verification token for a user is created.',
],
'update' => [
'$description' => 'This event triggers when a verification token for a user is validated.'
],
],
'create' => [
'$description' => 'This event triggers when a user is created.'
],
'delete' => [
'$description' => 'This event triggers when a user is deleted.',
],
'update' => [
'$description' => 'This event triggers when a user is updated.',
'email' => [
'$description' => 'This event triggers when a user\'s email address is updated.',
],
'name' => [
'$description' => 'This event triggers when a user\'s name is updated.',
],
'password' => [
'$description' => 'This event triggers when a user\'s password is updated.',
],
'status' => [
'$description' => 'This event triggers when a user\'s status is updated.',
],
'prefs' => [
'$description' => 'This event triggers when a user\'s preferences is updated.',
],
]
],
'account.update.email' => [
'description' => 'This event triggers when the account email address is updated.',
'model' => Response::MODEL_USER,
'note' => '',
'collections' => [
'$model' => Response::MODEL_COLLECTION,
'$resource' => true,
'$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 collection is created.'
],
'delete' => [
'$description' => 'This event triggers when a collection is deleted.',
],
'update' => [
'$description' => 'This event triggers when a collection is updated.',
]
],
'account.update.name' => [
'description' => 'This event triggers when the account name is updated.',
'model' => Response::MODEL_USER,
'note' => '',
'buckets' => [
'$model' => Response::MODEL_BUCKET,
'$resource' => true,
'$description' => 'This event triggers on any buckets event.',
'files' => [
'$model' => Response::MODEL_FILE,
'$resource' => true,
'$description' => 'This event triggers on any files event.',
'create' => [
'$description' => 'This event triggers when a file is created.',
],
'delete' => [
'$description' => 'This event triggers when a file is deleted.'
],
'update' => [
'$description' => 'This event triggers when a file is updated.'
],
],
'create' => [
'$description' => 'This event triggers when a bucket is created.'
],
'delete' => [
'$description' => 'This event triggers when a bucket is deleted.',
],
'update' => [
'$description' => 'This event triggers when a bucket is updated.',
]
],
'account.update.password' => [
'description' => 'This event triggers when the account password is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.email' => [
'description' => 'This event triggers when the user email address is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.name' => [
'description' => 'This event triggers when the user name is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'users.update.password' => [
'description' => 'This event triggers when the user password is updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'account.update.prefs' => [
'description' => 'This event triggers when the account preferences are updated.',
'model' => Response::MODEL_USER,
'note' => '',
],
'account.recovery.create' => [
'description' => 'This event triggers when the account recovery token is created.',
'model' => Response::MODEL_TOKEN,
'note' => 'version >= 0.7',
],
'account.recovery.update' => [
'description' => 'This event triggers when the account recovery token is validated.',
'model' => Response::MODEL_TOKEN,
'note' => 'version >= 0.7',
],
'account.verification.create' => [
'description' => 'This event triggers when the account verification token is created.',
'model' => Response::MODEL_TOKEN,
'note' => 'version >= 0.7',
],
'account.verification.update' => [
'description' => 'This event triggers when the account verification token is validated.',
'model' => Response::MODEL_TOKEN,
'note' => 'version >= 0.7',
],
'account.delete' => [
'description' => 'This event triggers when the account is deleted.',
'model' => Response::MODEL_USER,
'note' => '',
],
'account.sessions.create' => [
'description' => 'This event triggers when the account session is created.',
'model' => Response::MODEL_SESSION,
'note' => '',
],
'account.sessions.delete' => [
'description' => 'This event triggers when the account session is deleted.',
'model' => Response::MODEL_SESSION,
'note' => '',
],
'account.sessions.update' => [
'description' => 'This event triggers when the account session is updated.',
'model' => Response::MODEL_SESSION,
'note' => '',
],
'database.collections.create' => [
'description' => 'This event triggers when a database collection is created.',
'model' => Response::MODEL_COLLECTION,
'note' => '',
],
'database.collections.update' => [
'description' => 'This event triggers when a database collection is updated.',
'model' => Response::MODEL_COLLECTION,
'note' => '',
],
'database.collections.delete' => [
'description' => 'This event triggers when a database collection is deleted.',
'model' => Response::MODEL_COLLECTION,
'note' => '',
],
'database.attributes.create' => [
'description' => 'This event triggers when a collection attribute is created.',
'model' => Response::MODEL_ATTRIBUTE,
'note' => '',
],
'database.attributes.delete' => [
'description' => 'This event triggers when a collection attribute is deleted.',
'model' => Response::MODEL_ATTRIBUTE,
'note' => '',
],
'database.indexes.create' => [
'description' => 'This event triggers when a collection index is created.',
'model' => Response::MODEL_INDEX,
'note' => '',
],
'database.indexes.delete' => [
'description' => 'This event triggers when a collection index is deleted.',
'model' => Response::MODEL_INDEX,
'note' => '',
],
'database.documents.create' => [
'description' => 'This event triggers when a database document is created.',
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'database.documents.update' => [
'description' => 'This event triggers when a database document is updated.',
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'database.documents.delete' => [
'description' => 'This event triggers when a database document is deleted.',
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'functions.create' => [
'description' => 'This event triggers when a function is created.',
'model' => Response::MODEL_FUNCTION,
'note' => 'version >= 0.7',
],
'functions.update' => [
'description' => 'This event triggers when a function is updated.',
'model' => Response::MODEL_FUNCTION,
'note' => 'version >= 0.7',
],
'functions.delete' => [
'description' => 'This event triggers when a function is deleted.',
'model' => Response::MODEL_ANY,
'note' => 'version >= 0.7',
],
'functions.deployments.create' => [
'description' => 'This event triggers when a function delpoyment is created.',
'model' => Response::MODEL_DEPLOYMENT,
'note' => 'version >= 0.7',
],
'functions.deployments.update' => [
'description' => 'This event triggers when a function delpoyment is updated.',
'model' => Response::MODEL_FUNCTION,
'note' => 'version >= 0.7',
],
'functions.deployments.delete' => [
'description' => 'This event triggers when a function delpoyment is deleted.',
'model' => Response::MODEL_ANY,
'note' => 'version >= 0.7',
],
'functions.executions.create' => [
'description' => 'This event triggers when a function execution is created.',
'model' => Response::MODEL_EXECUTION,
'note' => 'version >= 0.7',
],
'functions.executions.update' => [
'description' => 'This event triggers when a function execution is updated.',
'model' => Response::MODEL_EXECUTION,
'note' => 'version >= 0.7',
],
'storage.files.create' => [
'description' => 'This event triggers when a storage file is created.',
'model' => Response::MODEL_FILE,
'note' => '',
],
'storage.files.update' => [
'description' => 'This event triggers when a storage file is updated.',
'model' => Response::MODEL_FILE,
'note' => '',
],
'storage.files.delete' => [
'description' => 'This event triggers when a storage file is deleted.',
'model' => Response::MODEL_FILE,
'note' => '',
],
'storage.buckets.create' => [
'description' => 'This event triggers when a storage bucket is created.',
'model' => Response::MODEL_BUCKET,
'note' => '',
],
'storage.buckets.update' => [
'description' => 'This event triggers when a storage bucket is updated.',
'model' => Response::MODEL_BUCKET,
'note' => '',
],
'storage.buckets.delete' => [
'description' => 'This event triggers when a storage bucket is deleted.',
'model' => Response::MODEL_BUCKET,
'note' => '',
],
'users.create' => [
'description' => 'This event triggers when a user is created from the users API.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.7',
],
'users.update.prefs' => [
'description' => 'This event triggers when a user preference is updated from the users API.',
'model' => Response::MODEL_ANY,
'note' => 'version >= 0.7',
],
'users.update.email' => [
'description' => 'This event triggers when the user email address is updated.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.10',
],
'users.update.name' => [
'description' => 'This event triggers when the user name is updated.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.10',
],
'users.update.password' => [
'description' => 'This event triggers when the user password is updated.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.10',
],
'users.update.status' => [
'description' => 'This event triggers when a user status is updated from the users API.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.7',
],
'users.delete' => [
'description' => 'This event triggers when a user is deleted from users API.',
'model' => Response::MODEL_USER,
'note' => 'version >= 0.7',
],
'users.sessions.delete' => [
'description' => 'This event triggers when a user session is deleted from users API.',
'model' => Response::MODEL_SESSION,
'note' => 'version >= 0.7',
],
'teams.create' => [
'description' => 'This event triggers when a team is created.',
'model' => Response::MODEL_TEAM,
'note' => 'version >= 0.7',
],
'teams.update' => [
'description' => 'This event triggers when a team is updated.',
'model' => Response::MODEL_TEAM,
'note' => 'version >= 0.7',
],
'teams.delete' => [
'description' => 'This event triggers when a team is deleted.',
'model' => Response::MODEL_TEAM,
'note' => 'version >= 0.7',
],
'teams.memberships.create' => [
'description' => 'This event triggers when a team memberships is created.',
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.7',
],
'teams.memberships.update' => [
'description' => 'This event triggers when a team membership is updated.',
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.8',
],
'teams.memberships.update.status' => [
'description' => 'This event triggers when a team memberships status is updated.',
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.7',
],
'teams.memberships.delete' => [
'description' => 'This event triggers when a team memberships is deleted.',
'model' => Response::MODEL_MEMBERSHIP,
'note' => 'version >= 0.7',
'teams' => [
'$model' => Response::MODEL_TEAM,
'$resource' => true,
'$description' => 'This event triggers on any teams event.',
'memberships' => [
'$model' => Response::MODEL_MEMBERSHIP,
'$resource' => true,
'$description' => 'This event triggers on any team memberships event.',
'create' => [
'$description' => 'This event triggers when a membership is created.',
],
'delete' => [
'$description' => 'This event triggers when a membership is deleted.'
],
'update' => [
'$description' => 'This event triggers when a membership is updated.',
'status' => [
'$description' => 'This event triggers when a team memberships status is updated.'
]
],
],
'create' => [
'$description' => 'This event triggers when a bucket is created.'
],
'delete' => [
'$description' => 'This event triggers when a bucket is deleted.',
],
'update' => [
'$description' => 'This event triggers when a bucket is updated.',
]
],
'functions' => [
'$model' => Response::MODEL_FUNCTION,
'$resource' => true,
'$description' => 'This event triggers on any functions event.',
'deployments' => [
'$model' => Response::MODEL_DEPLOYMENT,
'$resource' => true,
'$description' => 'This event triggers on any deployments event.',
'create' => [
'$description' => 'This event triggers when a deployment is created.',
],
'delete' => [
'$description' => 'This event triggers when a deployment is deleted.'
],
'update' => [
'$description' => 'This event triggers when a deployment is updated.'
],
],
'executions' => [
'$model' => Response::MODEL_EXECUTION,
'$resource' => true,
'$description' => 'This event triggers on any executions event.',
'create' => [
'$description' => 'This event triggers when an execution is created.',
],
'delete' => [
'$description' => 'This event triggers when an execution is deleted.'
],
'update' => [
'$description' => 'This event triggers when an execution is updated.'
],
],
'create' => [
'$description' => 'This event triggers when a function is created.'
],
'delete' => [
'$description' => 'This event triggers when a function is deleted.',
],
'update' => [
'$description' => 'This event triggers when a function is updated.',
]
]
];
+1 -1
View File
@@ -51,7 +51,7 @@ return [
'or', // Oriya
'tl', // Filipino
'pl', // Polish
'pt-br', // Portuguese - Brazil
'pt-br', // Portuguese - Brazil
'pt-pt', // Portuguese - Portugal
'pa', // Punjabi
'ro', // Romanian
+7 -1
View File
@@ -27,6 +27,12 @@
"emails.invitation.footer": "If you are not interested, you can ignore this message.",
"emails.invitation.thanks": "Thanks",
"emails.invitation.signature": "{{project}} team",
"emails.certificate.subject": "Certificate failure for %s",
"emails.certificate.hello": "Hello",
"emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}",
"emails.certificate.footer": "Your previous certificate will be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.",
"emails.certificate.thanks": "Thanks",
"emails.certificate.signature": "{{project}} team",
"locale.country.unknown": "Unknown",
"countries.af": "Afghanistan",
"countries.ao": "Angola",
@@ -229,4 +235,4 @@
"continents.na": "North America",
"continents.oc": "Oceania",
"continents.sa": "South America"
}
}
+2 -2
View File
@@ -17,13 +17,13 @@
"emails.magicSession.signature": "Equipo de {{project}}",
"emails.recovery.subject": "Restablecer contraseña",
"emails.recovery.hello": "Hola {{name}}",
"emails.recovery.body": "Haz clic en este enlace para restablecer la contraseña de tu proyecto {{project}}.",
"emails.recovery.body": "Haz clic en este enlace para restablecer la contraseña de {{project}}.",
"emails.recovery.footer": "Si no has solicitado restablecer la contraseña, puedes ignorar este mensaje.",
"emails.recovery.thanks": "Gracias",
"emails.recovery.signature": "Equipo de {{project}}",
"emails.invitation.subject": "Invitación al equipo %s en %s",
"emails.invitation.hello": "Hola",
"emails.invitation.body": "Este correo ha sido enviado a petición de {{owner}} quien quiere invitarte a formar parte del equipo {{team}} en el proyecto {{project}}.",
"emails.invitation.body": "Este correo ha sido enviado a petición de {{owner}} quien quiere invitarte a formar parte del equipo {{team}} en {{project}}.",
"emails.invitation.footer": "Si no estas interesado, puedes ignorar este mensaje.",
"emails.invitation.thanks": "Gracias",
"emails.invitation.signature": "Equipo de {{project}}",
+15 -9
View File
@@ -5,8 +5,8 @@
"emails.sender": "Équipe %s",
"emails.verification.subject": "Vérification du compte",
"emails.verification.hello": "Bonjour {{name}}",
"emails.verification.body": "Suivez ce lien pour vérifier votre adresse mail.",
"emails.verification.footer": "Si vous n'avez pas demandé à vérifier cette adresse mail, vous pouvez ignorer ce message.",
"emails.verification.body": "Suivez ce lien pour vérifier votre adresse e-mail.",
"emails.verification.footer": "Si vous n'avez pas demandé à vérifier cette adresse, vous pouvez ignorer ce message.",
"emails.verification.thanks": "Merci",
"emails.verification.signature": "Équipe {{project}}",
"emails.magicSession.subject": "Connexion",
@@ -14,19 +14,25 @@
"emails.magicSession.body": "Suivez ce lien pour vous connecter.",
"emails.magicSession.footer": "Si vous n'avez pas demandé à vous connecter en utilisant cet e-mail, vous pouvez ignorer ce message.",
"emails.magicSession.thanks": "Merci",
"emails.magicSession.signature": "Réinitialisation du mot de passe",
"emails.recovery.subject": "Bonjour {{name}}",
"emails.magicSession.signature": "L'équipe {{project}}",
"emails.recovery.subject": "Réinitialisation du mot de passe",
"emails.recovery.hello": "Bonjour {{name}}",
"emails.recovery.body": "Suivez ce lien pour réinitialiser votre mot de passe de {{projet}}.",
"emails.recovery.body": "Suivez ce lien pour réinitialiser votre mot de passe pour {{projet}}.",
"emails.recovery.footer": "Si vous n'avez pas demandé à réinitialiser votre mot de passe, vous pouvez ignorer ce message.",
"emails.recovery.thanks": "Merci",
"emails.recovery.signature": "Équipe {{project}}",
"emails.recovery.signature": "L'équipe {{project}}",
"emails.invitation.subject": "Invitation à l'équipe %s de %s",
"emails.invitation.hello": "Bonjour",
"emails.invitation.body": "Ce mail vous a été envoyé parce que {{owner}} voulait vous inviter à devenir membre de l'équipe {{team}} de {{project}}.",
"emails.invitation.body": "Cet e-mail vous a été envoyé parce que {{owner}} souhaite vous inviter à devenir membre de l'équipe {{team}} pour {{project}}.",
"emails.invitation.footer": "Si vous n'êtes pas intéressé, vous pouvez ignorer ce message.",
"emails.invitation.thanks": "Merci",
"emails.invitation.signature": "Équipe {{project}}",
"emails.invitation.signature": "L'équipe {{project}}",
"emails.certificate.subject": "Échec du certificat pour %s",
"emails.certificate.hello": "Bonjour",
"emails.certificate.body": "Le certificate pour votre domaine '{{domain}}' n'a pas pu être généré. Ceci est la tentative {{tentative}} et l'échec a été causé par : {{erreur}}",
"emails.certificate.footer": "Votre certificat précédent sera valide pendant 30 jours à compter de la première défaillance. Nous vous recommandons fortement d'enquêter sur ce cas, sinon votre domaine se retrouvera sans communication SSL valide.",
"emails.certificate.thanks": "Merci",
"emails.certificate.signature": "L'équipe {{project}}",
"locale.country.unknown": "Inconnu",
"countries.af": "Afghanistan",
"countries.ao": "Angola",
@@ -229,4 +235,4 @@
"continents.na": "Amérique du Nord",
"continents.oc": "Océanie",
"continents.sa": "Amérique du Sud"
}
}
+3 -3
View File
@@ -18,12 +18,12 @@
"emails.recovery.subject": "രഹസ്യവാക്ക് പുനക്രമീകരണം",
"emails.recovery.hello": "നമസ്കാരം {{name}}",
"emails.recovery.body": "നിങ്ങളുടെ {{Project}} രഹസ്യവാക്ക് പുനക്രമീകരിക്കുന്നതിന് ഈ ലിങ്ക് പിന്തുടരുക.",
"emails.recovery.footer": "നിങ്ങളുടെ പാസ്‌വേഡ് പുനക്രമീകരിക്കാന്‍ നിങ്ങൾ ആവശ്യപ്പെട്ടില്ലെങ്കിൽ, ഈ സന്ദേശം അവഗണിക്കാവുന്നതാണ്.",
"emails.recovery.footer": "നിങ്ങളുടെ രഹസ്യവാക്ക് പുനക്രമീകരിക്കാന്‍ നിങ്ങൾ ആവശ്യപ്പെട്ടില്ലെങ്കിൽ, ഈ സന്ദേശം അവഗണിക്കാവുന്നതാണ്.",
"emails.recovery.thanks": "നന്ദി",
"emails.recovery.signature": "{{project}} ടീം",
"emails.invitation.subject": "%s -ലെ %s ടീമിലേക്കുള്ള ക്ഷണം",
"emails.invitation.hello": "നമസ്കാരം",
"emails.invitation.body": "നിങ്ങളെ {{project}} -ലെ {{team}} ടീമിലെ അംഗമാകുവാന്‍ ക്ഷണിക്കാൻ {{owner}} ആഗ്രഹിക്കുതിനാലാണ് ഈ മെയിൽ നിങ്ങൾക്ക് അയക്കുന്നത്.",
"emails.invitation.body": "നിങ്ങളെ {{project}} -ലെ {{team}} ടീമിലെ അംഗമാകുവാന്‍ ക്ഷണിക്കാൻ {{owner}} ആഗ്രഹിക്കുന്നതിനാലാണ് ഈ മെയിൽ നിങ്ങൾക്ക് അയക്കുന്നത്.",
"emails.invitation.footer": "നിങ്ങൾക്ക് താൽപ്പര്യമില്ലെങ്കിൽ, ഈ സന്ദേശം അവഗണിക്കാവുന്നതാണ്.",
"emails.invitation.thanks": "നന്ദി",
"emails.invitation.signature": "{{project}} ടീം",
@@ -229,4 +229,4 @@
"continents.na": "വടക്കേ അമേരിക്ക",
"continents.oc": "ഓഷ്യാനിയ",
"continents.sa": "തെക്കേ അമേരിക്ക"
}
}
+14 -14
View File
@@ -15,7 +15,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '7.0.0',
'version' => '8.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' => '4.0.2',
'version' => '5.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.4.0',
'version' => '0.5.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.5.0',
'version' => '0.6.1',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -180,7 +180,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '0.16.0',
'version' => '0.17.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -193,7 +193,7 @@ return [
'gitUrl' => 'git@github.com:appwrite/sdk-for-cli.git',
'gitRepoName' => 'sdk-for-cli',
'gitUserName' => 'appwrite',
'gitBranch' => 'main',
'gitBranch' => 'master',
],
],
],
@@ -208,7 +208,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '5.1.0',
'version' => '6.0.0',
'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' => '3.1.0',
'version' => '4.0.0',
'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' => '4.1.0',
'version' => '5.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.8.0',
'version' => '0.9.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' => '4.1.0',
'version' => '5.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' => '4.0.2',
'version' => '5.0.1',
'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.4.0',
'version' => '0.5.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.4.0',
'version' => '0.5.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
+20 -30
View File
@@ -211,6 +211,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
'tradeshift' => [
'name' => 'Tradeshift',
'developers' => 'https://developers.tradeshift.com/docs/api',
@@ -241,25 +251,15 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
'vk' => [
'name' => 'VK',
'developers' => 'https://vk.com/dev',
'icon' => 'icon-vk',
'wordpress' => [
'name' => 'WordPress',
'developers' => 'https://developer.wordpress.com/docs/oauth2/',
'icon' => 'icon-wordpress',
'enabled' => true,
'sandbox' => false,
'form' => false,
'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,
'mock' => false
],
'yahoo' => [
'name' => 'Yahoo',
@@ -307,25 +307,15 @@ return [ // Ordered by ABC.
// 'beta' => false,
// 'mock' => false,
// ],
'wordpress' => [
'name' => 'WordPress',
'developers' => 'https://developer.wordpress.com/docs/oauth2/',
'icon' => 'icon-wordpress',
'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
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
'mock' => false,
],
// Keep Last
'mock' => [
+6 -5
View File
@@ -1,15 +1,16 @@
<?php
use Utopia\App;
use Appwrite\Runtimes\Runtimes;
/**
* List of Appwrite Cloud Functions supported runtimes
*/
$runtimes = new Runtimes();
use Utopia\App;
use Appwrite\Runtimes\Runtimes;
$runtimes = new Runtimes('v1');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
$runtimes = $runtimes->getAll(true, $allowList);
return $runtimes;
return $runtimes;
+1 -1
View File
@@ -70,4 +70,4 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
];;
];
+1 -1
View File
@@ -43,7 +43,7 @@ return [
'avatars' => [
'key' => 'avatars',
'name' => 'Avatars',
'subtitle'=> 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.',
'subtitle' => 'The Avatars service aims to help you complete everyday tasks related to your app image, icons, and avatars.',
'description' => '/docs/services/avatars.md',
'controller' => 'api/avatars.php',
'sdk' => true,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,4 +5,4 @@ return [ // Accepted inputs files
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'png' => 'image/png',
];
];
+16 -12
View File
@@ -1,18 +1,22 @@
<?php
return [ // Based on this list @see http://stackoverflow.com/a/4212908/2299554
'default' => __DIR__.'/logos/none.png',
'default_image' => __DIR__.'/logos/image.png',
'default' => __DIR__ . '/logos/none.png',
'default_image' => __DIR__ . '/logos/image.png',
// Video Files
'video/mp4' => __DIR__.'/logos/video.png',
'video/x-flv' => __DIR__.'/logos/video.png',
'application/x-mpegURL' => __DIR__.'/logos/video.png',
'video/MP2T' => __DIR__.'/logos/video.png',
'video/3gpp' => __DIR__.'/logos/video.png',
'video/quicktime' => __DIR__.'/logos/video.png',
'video/x-msvideo' => __DIR__.'/logos/video.png',
'video/x-ms-wmv' => __DIR__.'/logos/video.png',
'video/mp4' => __DIR__ . '/logos/video.png',
'video/x-flv' => __DIR__ . '/logos/video.png',
'video/webm' => __DIR__ . '/logos/video.png',
'application/x-mpegURL' => __DIR__ . '/logos/video.png',
'video/MP2T' => __DIR__ . '/logos/video.png',
'video/3gpp' => __DIR__ . '/logos/video.png',
'video/quicktime' => __DIR__ . '/logos/video.png',
'video/x-msvideo' => __DIR__ . '/logos/video.png',
'video/x-ms-wmv' => __DIR__ . '/logos/video.png',
// // Microsoft Word
// 'application/msword' => __DIR__.'/logos/word.png',
@@ -41,4 +45,4 @@ return [ // Based on this list @see http://stackoverflow.com/a/4212908/2299554
// Adobe PDF
// 'application/pdf' => __DIR__.'/logos/pdf.png',
];
];
+3 -2
View File
@@ -10,6 +10,7 @@ return [
// Video Files
'video/mp4',
'video/x-flv',
'video/webm',
'application/x-mpegURL',
'video/MP2T',
'video/3gpp',
@@ -30,7 +31,7 @@ return [
'audio/ogg', // Ogg Vorbis RFC 5334
'audio/vorbis', // Vorbis RFC 5215
'audio/vnd.wav', // wav RFC 2361
// Microsoft Word
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -61,4 +62,4 @@ return [
// Adobe PDF
'application/pdf',
];
];
+1 -1
View File
@@ -6,4 +6,4 @@ return [ // Accepted outputs files
'gif' => 'image/gif',
'png' => 'image/png',
'webp' => 'image/webp',
];
];
+99 -3
View File
@@ -440,7 +440,7 @@ return [
],
[
'name' => '_APP_STORAGE_DEVICE',
'description' => 'Select default storage device. The default value is \'Local\'. List of supported adapters are \'Local\', \'S3\' and \'DOSpaces\'.',
'description' => 'Select default storage device. The default value is \'Local\'. List of supported adapters are \'Local\', \'S3\', \'DOSpaces\', \'Backblaze\', \'Linode\' and \'Wasabi\'.',
'introduction' => '0.13.0',
'default' => 'Local',
'required' => false,
@@ -466,7 +466,7 @@ return [
'name' => '_APP_STORAGE_S3_REGION',
'description' => 'AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console.',
'introduction' => '0.13.0',
'default' => 'us-eas-1',
'default' => 'us-east-1',
'required' => false,
'question' => '',
],
@@ -498,7 +498,7 @@ return [
'name' => '_APP_STORAGE_DO_SPACES_REGION',
'description' => 'DigitalOcean spaces region. Required when storage adapter is set to DOSpaces. You can find your region info for your space from DigitalOcean console.',
'introduction' => '0.13.0',
'default' => 'us-eas-1',
'default' => 'us-east-1',
'required' => false,
'question' => '',
],
@@ -510,6 +510,102 @@ return [
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_BACKBLAZE_ACCESS_KEY',
'description' => 'Backblaze access key. Required when the storage adapter is set to Backblaze. Your Backblaze keyID will be your access key. You can get your keyID from your Backblaze console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_BACKBLAZE_SECRET',
'description' => 'Backblaze secret key. Required when the storage adapter is set to Backblaze. Your Backblaze applicationKey will be your secret key. You can get your applicationKey from your Backblaze console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_BACKBLAZE_REGION',
'description' => 'Backblaze region. Required when storage adapter is set to Backblaze. You can find your region info from your Backblaze console.',
'introduction' => '0.14.2',
'default' => 'us-west-004',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_BACKBLAZE_BUCKET',
'description' => 'Backblaze bucket. Required when storage adapter is set to Backblaze. You can create your bucket from your Backblaze console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_LINODE_ACCESS_KEY',
'description' => 'Linode object storage access key. Required when the storage adapter is set to Linode. You can get your access key from your Linode console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_LINODE_SECRET',
'description' => 'Linode object storage secret key. Required when the storage adapter is set to Linode. You can get your secret key from your Linode console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_LINODE_REGION',
'description' => 'Linode object storage region. Required when storage adapter is set to Linode. You can find your region info from your Linode console.',
'introduction' => '0.14.2',
'default' => 'eu-central-1',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_LINODE_BUCKET',
'description' => 'Linode object storage bucket. Required when storage adapter is set to Linode. You can create buckets in your Linode console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_WASABI_ACCESS_KEY',
'description' => 'Wasabi access key. Required when the storage adapter is set to Wasabi. You can get your access key from your Wasabi console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_WASABI_SECRET',
'description' => 'Wasabi secret key. Required when the storage adapter is set to Wasabi. You can get your secret key from your Wasabi console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_WASABI_REGION',
'description' => 'Wasabi region. Required when storage adapter is set to Wasabi. You can find your region info from your Wasabi console.',
'introduction' => '0.14.2',
'default' => 'eu-central-1',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_WASABI_BUCKET',
'description' => 'Wasabi bucket. Required when storage adapter is set to Wasabi. You can create buckets in your Wasabi console.',
'introduction' => '0.14.2',
'default' => '',
'required' => false,
'question' => '',
],
],
],
[
File diff suppressed because it is too large Load Diff
+24 -30
View File
@@ -16,9 +16,9 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\Validator\WhiteList;
$avatarCallback = function ($type, $code, $width, $height, $quality, Response $response) {
$avatarCallback = function (string $type, string $code, int $width, int $height, int $quality, Response $response) {
$code = \strtolower($code);
$type = \strtolower($type);
@@ -38,7 +38,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality, Response $r
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v1/avatars/'.$type.'/:code-' . $code . $width . $height . $quality . $output);
$key = \md5('/v1/avatars/' . $type . '/:code-' . $code . $width . $height . $quality . $output);
$path = $set[$code];
$type = 'png';
@@ -56,8 +56,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality, Response $r
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
->send($data);
}
$image = new Image(\file_get_contents($path));
@@ -95,7 +94,7 @@ App::get('/v1/avatars/credit-cards/:code')
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
->inject('response')
->action(fn($code, $width, $height, $quality, $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response));
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/browsers/:code')
->desc('Get Browser Icon')
@@ -113,7 +112,7 @@ App::get('/v1/avatars/browsers/:code')
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
->inject('response')
->action(fn($code, $width, $height, $quality, $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response));
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/flags/:code')
->desc('Get Country Flag')
@@ -131,7 +130,7 @@ App::get('/v1/avatars/flags/:code')
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
->inject('response')
->action(fn($code, $width, $height, $quality, $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response));
->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response));
App::get('/v1/avatars/image')
->desc('Get Image from URL')
@@ -145,10 +144,10 @@ App::get('/v1/avatars/image')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.')
->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000.', true)
->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000.', true)
->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000. Defaults to 400.', true)
->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000. Defaults to 400.', true)
->inject('response')
->action(function ($url, $width, $height, Response $response) {
->action(function (string $url, int $width, int $height, Response $response) {
$quality = 80;
$output = 'png';
@@ -163,8 +162,7 @@ App::get('/v1/avatars/image')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
->send($data);
}
if (!\extension_loaded('imagick')) {
@@ -179,7 +177,7 @@ App::get('/v1/avatars/image')
try {
$image = new Image($fetch);
} catch (\Exception$exception) {
} catch (\Exception $exception) {
throw new Exception('Unable to parse image', 500, Exception::GENERAL_SERVER_ERROR);
}
@@ -196,7 +194,6 @@ App::get('/v1/avatars/image')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
;
unset($image);
});
@@ -214,7 +211,7 @@ App::get('/v1/avatars/favicon')
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.')
->inject('response')
->action(function ($url, Response $response) {
->action(function (string $url, Response $response) {
$width = 56;
$height = 56;
@@ -231,8 +228,7 @@ App::get('/v1/avatars/favicon')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
->send($data);
}
if (!\extension_loaded('imagick')) {
@@ -246,7 +242,8 @@ App::get('/v1/avatars/favicon')
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_URL => $url,
CURLOPT_USERAGENT => \sprintf(APP_USERAGENT,
CURLOPT_USERAGENT => \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
),
@@ -324,8 +321,7 @@ App::get('/v1/avatars/favicon')
->setContentType('image/x-icon')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data)
;
->send($data);
}
$fetch = @\file_get_contents($outputHref, false);
@@ -365,11 +361,11 @@ App::get('/v1/avatars/qr')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE_PNG)
->param('text', '', new Text(512), 'Plain text to be converted to QR code image.')
->param('size', 400, new Range(0, 1000), 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true)
->param('size', 400, new Range(1, 1000), 'QR code size. Pass an integer between 1 to 1000. Defaults to 400.', true)
->param('margin', 1, new Range(0, 10), 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true)
->param('download', false, new Boolean(true), 'Return resulting image with \'Content-Disposition: attachment \' headers for the browser to start downloading it. Pass 0 for no header, or 1 for otherwise. Default value is set to 0.', true)
->inject('response')
->action(function ($text, $size, $margin, $download, Response $response) {
->action(function (string $text, int $size, int $margin, bool $download, Response $response) {
$download = ($download === '1' || $download === 'true' || $download === 1 || $download === true);
$options = new QROptions([
@@ -391,8 +387,7 @@ App::get('/v1/avatars/qr')
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png')
->send($image->output('png', 9))
;
->send($image->output('png', 9));
});
App::get('/v1/avatars/initials')
@@ -413,7 +408,7 @@ App::get('/v1/avatars/initials')
->param('background', '', new HexColor(), 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true)
->inject('response')
->inject('user')
->action(function ($name, $width, $height, $color, $background, Response $response, Document $user) {
->action(function (string $name, int $width, int $height, string $color, string $background, Response $response, Document $user) {
$themes = [
['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET
@@ -433,8 +428,8 @@ App::get('/v1/avatars/initials')
$name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', ''));
$words = \explode(' ', \strtoupper($name));
// if there is no space, try to split by `_` underscore
$words = (count($words) == 1 ) ? \explode('_', \strtoupper($name)) : $words;
$words = (count($words) == 1) ? \explode('_', \strtoupper($name)) : $words;
$initials = null;
$code = 0;
@@ -473,6 +468,5 @@ App::get('/v1/avatars/initials')
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png')
->send($image->getImageBlob())
;
->send($image->getImageBlob());
});
File diff suppressed because it is too large Load Diff
+157 -164
View File
@@ -2,15 +2,22 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Validator\Event as ValidatorEvent;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\Validator\UID;
use Appwrite\Stats\Stats;
use Utopia\Storage\Device;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileExt;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Appwrite\Utopia\Response;
use Utopia\Swoole\Request;
use Appwrite\Task\Validator\Cron;
use Utopia\App;
use Utopia\Database\Database;
@@ -34,7 +41,7 @@ App::post('/v1/functions')
->groups(['api', 'functions'])
->desc('Create Function')
->label('scope', 'functions.write')
->label('event', 'functions.create')
->label('event', 'functions.[functionId].create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'create')
@@ -44,17 +51,16 @@ App::post('/v1/functions')
->label('sdk.response.model', Response::MODEL_FUNCTION)
->param('functionId', '', new CustomId(), 'Function 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('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('execute', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $name, $execute, $runtime, $vars, $events, $schedule, $timeout, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->inject('events')
->action(function (string $functionId, string $name, array $execute, string $runtime, array $vars, array $events, string $schedule, int $timeout, Response $response, Database $dbForProject, Event $eventsInstance) {
$functionId = ($functionId == 'unique()') ? $dbForProject->getId() : $functionId;
$function = $dbForProject->createDocument('functions', new Document([
@@ -75,6 +81,8 @@ App::post('/v1/functions')
'search' => implode(' ', [$functionId, $name, $runtime]),
]));
$eventsInstance->setParam('functionId', $function->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($function, Response::MODEL_FUNCTION);
});
@@ -98,9 +106,7 @@ App::get('/v1/functions')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
if (!empty($cursor)) {
$cursorFunction = $dbForProject->getDocument('functions', $cursor);
@@ -134,8 +140,7 @@ App::get('/v1/functions/runtimes')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_RUNTIME_LIST)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$runtimes = Config::getParam('runtimes');
@@ -144,7 +149,7 @@ App::get('/v1/functions/runtimes')
return $runtimes[$key];
}, array_keys($runtimes));
$response->dynamic(new Document([
$response->dynamic(new Document([
'total' => count($runtimes),
'runtimes' => $runtimes
]), Response::MODEL_RUNTIME_LIST);
@@ -164,10 +169,7 @@ App::get('/v1/functions/:functionId')
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $functionId, Response $response, Database $dbForProject) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -191,20 +193,16 @@ App::get('/v1/functions/:functionId/usage')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $range, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Registry\Registry $register */
->action(function (string $functionId, string $range, Response $response, Database $dbForProject) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND);
}
$usage = [];
if(App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$periods = [
'24h' => [
'period' => '30m',
@@ -223,16 +221,16 @@ App::get('/v1/functions/:functionId/usage')
'limit' => 90,
],
];
$metrics = [
"functions.$functionId.executions",
"functions.$functionId.failures",
"functions.$functionId.executions",
"functions.$functionId.failures",
"functions.$functionId.compute"
];
$stats = [];
Authorization::skip(function() use ($dbForProject, $periods, $range, $metrics, &$stats) {
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
@@ -241,7 +239,7 @@ App::get('/v1/functions/:functionId/usage')
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
@@ -254,7 +252,7 @@ App::get('/v1/functions/:functionId/usage')
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
@@ -265,7 +263,7 @@ App::get('/v1/functions/:functionId/usage')
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([
@@ -283,7 +281,7 @@ App::put('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Update Function')
->label('scope', 'functions.write')
->label('event', 'functions.update')
->label('event', 'functions.[functionId].update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'update')
@@ -293,20 +291,17 @@ App::put('/v1/functions/:functionId')
->label('sdk.response.model', Response::MODEL_FUNCTION)
->param('functionId', '', new UID(), 'Function ID.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('execute', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->action(function ($functionId, $name, $execute, $vars, $events, $schedule, $timeout, $response, $dbForProject, $project, $user) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
/** @var Appwrite\Auth\User $user */
->inject('events')
->action(function (string $functionId, string $name, array $execute, array $vars, array $events, string $schedule, int $timeout, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventsInstance) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -331,16 +326,19 @@ App::put('/v1/functions/:functionId')
])));
if ($next && $schedule !== $original) {
ResqueScheduler::enqueueAt($next, Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [
'projectId' => $project->getId(),
'webhooks' => $project->getAttribute('webhooks', []),
'functionId' => $function->getId(),
'userId' => $user->getId(),
'executionId' => null,
'trigger' => 'schedule',
]); // Async task rescheduale
// Async task reschedule
$functionEvent = new Func();
$functionEvent
->setFunction($function)
->setType('schedule')
->setUser($user)
->setProject($project);
$functionEvent->schedule($next);
}
$eventsInstance->setParam('functionId', $function->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
});
@@ -348,7 +346,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->groups(['api', 'functions'])
->desc('Update Function Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.deployments.update')
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'updateDeployment')
@@ -361,10 +359,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->inject('response')
->inject('dbForProject')
->inject('project')
->action(function ($functionId, $deploymentId, $response, $dbForProject, $project) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
->inject('events')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $events) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@@ -396,14 +392,18 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
])));
if ($next) { // Init first schedule
ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [
'projectId' => $project->getId(),
'webhooks' => $project->getAttribute('webhooks', []),
'functionId' => $function->getId(),
'executionId' => null,
'trigger' => 'schedule',
]); // Async task rescheduale
$functionEvent = new Func();
$functionEvent
->setType('schedule')
->setFunction($function)
->setProject($project);
$functionEvent->schedule($next);
}
$events
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
});
@@ -411,7 +411,7 @@ App::delete('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Delete Function')
->label('scope', 'functions.write')
->label('event', 'functions.delete')
->label('event', 'functions.[functionId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'delete')
@@ -422,10 +422,8 @@ App::delete('/v1/functions/:functionId')
->inject('response')
->inject('dbForProject')
->inject('deletes')
->action(function ($functionId, $response, $dbForProject, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $deletes */
->inject('events')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $deletes, Event $events) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -438,9 +436,10 @@ App::delete('/v1/functions/:functionId')
}
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $function)
;
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($function);
$events->setParam('functionId', $function->getId());
$response->dynamic(new Document(), Response::MODEL_NONE);
});
@@ -449,7 +448,7 @@ App::post('/v1/functions/:functionId/deployments')
->groups(['api', 'functions'])
->desc('Create Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.deployments.create')
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'createDeployment')
@@ -467,19 +466,11 @@ App::post('/v1/functions/:functionId/deployments')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('user')
->inject('events')
->inject('project')
->inject('deviceFunctions')
->inject('deviceLocal')
->action(function ($functionId, $entrypoint, $file, $activate, $request, $response, $dbForProject, $usage, $user, $project, $deviceFunctions, $deviceLocal) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $usage */
/** @var Appwrite\Auth\User $user */
/** @var Appwrite\Database\Document $project */
/** @var Utopia\Storage\Device $deviceFunctions */
/** @var Utopia\Storage\Device $deviceLocal */
->action(function (string $functionId, string $entrypoint, array $file, bool $activate, Request $request, Response $response, Database $dbForProject, Stats $usage, Event $events, Document $project, Device $deviceFunctions, Device $deviceLocal) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -515,7 +506,7 @@ App::post('/v1/functions/:functionId/deployments')
$end = $request->getContentRangeEnd();
$fileSize = $request->getContentRangeSize();
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
if(is_null($start) || is_null($end) || is_null($fileSize)) {
if (is_null($start) || is_null($end) || is_null($fileSize)) {
throw new Exception('Invalid content-range header', 400, Exception::STORAGE_INVALID_CONTENT_RANGE);
}
@@ -539,8 +530,8 @@ App::post('/v1/functions/:functionId/deployments')
// Save to storage
$fileSize ??= $deviceLocal->getFileSize($fileTmpName);
$path = $deviceFunctions->getPath($deploymentId.'.'.\pathinfo($fileName, PATHINFO_EXTENSION));
$path = $deviceFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)];
@@ -560,7 +551,7 @@ App::post('/v1/functions/:functionId/deployments')
$activate = (bool) filter_var($activate, FILTER_VALIDATE_BOOLEAN);
if($chunksUploaded === $chunks) {
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
@@ -574,7 +565,7 @@ App::post('/v1/functions/:functionId/deployments')
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
}
}
$fileSize = $deviceFunctions->getFileSize($path);
if ($deployment->isEmpty()) {
@@ -596,19 +587,18 @@ App::post('/v1/functions/:functionId/deployments')
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
}
// Enqueue a message to start the build
Resque::enqueue(Event::BUILDS_QUEUE_NAME, Event::BUILDS_CLASS_NAME, [
'projectId' => $project->getId(),
'resourceId' => $function->getId(),
'deploymentId' => $deploymentId,
'type' => BUILD_TYPE_DEPLOYMENT
]);
// Start the build
$buildEvent = new Build();
$buildEvent
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment)
->setProject($project)
->trigger();
$usage
->setParam('storage', $deployment->getAttribute('size', 0))
;
$usage->setParam('storage', $deployment->getAttribute('size', 0));
} else {
if($deployment->isEmpty()) {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$read' => ['role:all'],
@@ -632,6 +622,10 @@ App::post('/v1/functions/:functionId/deployments')
$metadata = null;
$events
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
});
@@ -656,9 +650,7 @@ App::get('/v1/functions/:functionId/deployments')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $functionId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -670,7 +662,6 @@ App::get('/v1/functions/:functionId/deployments')
$cursorDeployment = $dbForProject->getDocument('deployments', $cursor);
if ($cursorDeployment->isEmpty()) {
// TODO: Shouldn't this be a 404 error ?
throw new Exception("Tag '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND);
}
}
@@ -715,9 +706,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $deploymentId, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -742,7 +731,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
->groups(['api', 'functions'])
->desc('Delete Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.deployments.delete')
->label('event', 'functions.[functionId].deployments.[deploymentId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'deleteDeployment')
@@ -755,19 +744,15 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
->inject('dbForProject')
->inject('usage')
->inject('deletes')
->inject('events')
->inject('deviceFunctions')
->action(function ($functionId, $deploymentId, $response, $dbForProject, $usage, $deletes, $deviceFunctions) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $usage */
/** @var Appwrite\Event\Event $deletes */
/** @var Utopia\Storage\Device $deviceFunctions */
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Stats $usage, Delete $deletes, Event $events, Device $deviceFunctions) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404, Exception::DEPLOYMENT_NOT_FOUND);
@@ -783,20 +768,22 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
}
}
if($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment
if ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'deployment' => '',
])));
}
$usage
->setParam('storage', $deployment->getAttribute('size', 0) * -1)
;
->setParam('storage', $deployment->getAttribute('size', 0) * -1);
$events
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $deployment)
;
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($deployment);
$response->dynamic(new Document(), Response::MODEL_NONE);
});
@@ -805,7 +792,7 @@ App::post('/v1/functions/:functionId/executions')
->groups(['api', 'functions'])
->desc('Create Execution')
->label('scope', 'execution.write')
->label('event', 'functions.executions.create')
->label('event', 'functions.[functionId].executions.[executionId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'createExecution')
@@ -822,13 +809,10 @@ App::post('/v1/functions/:functionId/executions')
->inject('project')
->inject('dbForProject')
->inject('user')
->action(function ($functionId, $data, $async, $response, $project, $dbForProject, $user) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $user */
->inject('events')
->action(function (string $functionId, string $data, bool $async, Response $response, Document $project, Database $dbForProject, Document $user, Event $events) {
$function = Authorization::skip(fn() => $dbForProject->getDocument('functions', $functionId));
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty()) {
throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND);
@@ -842,18 +826,18 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400, Exception::FUNCTION_RUNTIME_UNSUPPORTED);
}
$deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception('Deployment not found. Deploy deployment before trying to execute a function', 404, Exception::DEPLOYMENT_NOT_FOUND);
throw new Exception('Deployment not found. Create a deployment before trying to execute a function', 404, Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found. Deploy deployment before trying to execute a function', 404, Exception::DEPLOYMENT_NOT_FOUND);
throw new Exception('Deployment not found. Create a deployment before trying to execute a function', 404, Exception::DEPLOYMENT_NOT_FOUND);
}
/** Check if build has completed */
$build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new Exception('Build not found', 404, Exception::BUILD_NOT_FOUND);
}
@@ -870,7 +854,7 @@ App::post('/v1/functions/:functionId/executions')
$executionId = $dbForProject->getId();
$execution = Authorization::skip(fn() => $dbForProject->createDocument('executions', new Document([
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [],
'$write' => [],
@@ -880,7 +864,7 @@ App::post('/v1/functions/:functionId/executions')
'trigger' => 'http', // http / schedule / event
'status' => 'waiting', // waiting / processing / completed / failed
'statusCode' => 0,
'stdout' => '',
'response' => '',
'stderr' => '',
'time' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
@@ -888,17 +872,17 @@ App::post('/v1/functions/:functionId/executions')
$jwt = ''; // initialize
if (!$user->isEmpty()) { // If userId exists, generate a JWT for function
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$current = $session;
}
}
if(!$current->isEmpty()) {
if (!$current->isEmpty()) {
$jwtObj = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
$jwt = $jwtObj->encode([
'userId' => $user->getId(),
@@ -907,19 +891,26 @@ App::post('/v1/functions/:functionId/executions')
}
}
$events
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setContext($function);
if ($async) {
Resque::enqueue(Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [
'projectId' => $project->getId(),
'functionId' => $function->getId(),
'webhooks' => $project->getAttribute('webhooks', []),
'executionId' => $execution->getId(),
'trigger' => 'http',
'data' => $data,
'userId' => $user->getId(),
'jwt' => $jwt,
]);
$event = new Func();
$event
->setType('http')
->setExecution($execution)
->setFunction($function)
->setData($data)
->setJWT($jwt)
->setProject($project)
->setUser($user);
$event->trigger();
$response->setStatusCode(Response::STATUS_CODE_CREATED);
return $response->dynamic($execution, Response::MODEL_EXECUTION);
}
@@ -956,17 +947,20 @@ App::post('/v1/functions/:functionId/executions')
/** Update execution status */
$execution->setAttribute('status', $executionResponse['status']);
$execution->setAttribute('statusCode', $executionResponse['statusCode']);
$execution->setAttribute('stdout', $executionResponse['stdout']);
$execution->setAttribute('response', $executionResponse['response']);
$execution->setAttribute('stderr', $executionResponse['stderr']);
$execution->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
$endtime = \microtime(true);
$time = $endtime - $execution->getAttribute('dateCreated');
$execution->setAttribute('time', $time);
$execution->setAttribute('status', 'failed');
$execution->setAttribute('statusCode', $th->getCode());
$execution->setAttribute('stderr', $th->getMessage());
Console::error($th->getMessage());
}
Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution));
Authorization::skip(fn () => $dbForProject->updateDocument('executions', $executionId, $execution));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -992,11 +986,9 @@ App::get('/v1/functions/:functionId/executions')
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $limit, $offset, $search, $cursor, $cursorDirection, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $functionId, int $limit, int $offset, string $search, string $cursor, string $cursorDirection, Response $response, Database $dbForProject) {
$function = Authorization::skip(fn() => $dbForProject->getDocument('functions', $functionId));
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty()) {
throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND);
@@ -1042,11 +1034,9 @@ App::get('/v1/functions/:functionId/executions/:executionId')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $executionId, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject) {
$function = Authorization::skip(fn() => $dbForProject->getDocument('functions', $functionId));
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty()) {
throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND);
@@ -1069,7 +1059,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->groups(['api', 'functions'])
->desc('Retry Build')
->label('scope', 'functions.write')
->label('event', 'functions.deployments.update')
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'retryBuild')
@@ -1082,10 +1072,8 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->inject('response')
->inject('dbForProject')
->inject('project')
->action(function ($functionId, $deploymentId, $buildId, $response, $dbForProject, $project) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
->inject('events')
->action(function (string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Document $project, Event $events) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@@ -1098,7 +1086,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
throw new Exception('Deployment not found', 404, Exception::DEPLOYMENT_NOT_FOUND);
}
$build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $buildId));
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $buildId));
if ($build->isEmpty()) {
throw new Exception('Build not found', 404, Exception::BUILD_NOT_FOUND);
@@ -1108,13 +1096,18 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
throw new Exception('Build not failed', 400, Exception::BUILD_IN_PROGRESS);
}
// Enqueue a message to start the build
Resque::enqueue(Event::BUILDS_QUEUE_NAME, Event::BUILDS_CLASS_NAME, [
'projectId' => $project->getId(),
'resourceId' => $function->getId(),
'deploymentId' => $deploymentId,
'type' => BUILD_TYPE_RETRY
]);
$events
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
// Retry the build
$buildEvent = new Build();
$buildEvent
->setType(BUILD_TYPE_RETRY)
->setResource($function)
->setDeployment($deployment)
->setProject($project)
->trigger();
$response->noContent();
});
});
+3
View File
@@ -39,6 +39,9 @@ App::get('/v1/graphql')
->inject('gqlSchema')
->action(Closure::fromCallable('graphqlRequest'));
use Appwrite\Extend\Exception;
use Utopia\App;
App::post('/v1/graphql')
->desc('GraphQL Endpoint')
->groups(['api', 'grapgql'])
+34 -63
View File
@@ -1,13 +1,15 @@
<?php
use Appwrite\Utopia\Response;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
App::get('/v1/health')
->desc('Get HTTP')
@@ -21,8 +23,7 @@ App::get('/v1/health')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$output = [
'status' => 'pass',
@@ -40,8 +41,7 @@ App::get('/v1/health/version')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_VERSION)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION);
});
@@ -59,9 +59,7 @@ App::get('/v1/health/db')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('utopia')
->action(function ($response, $utopia) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\App $utopia */
->action(function (Response $response, App $utopia) {
$checkStart = \microtime(true);
@@ -70,9 +68,9 @@ App::get('/v1/health/db')
// Run a small test to check the connection
$statement = $db->prepare("SELECT 1;");
$statement->closeCursor();
$statement->execute();
} catch (Exception $_e) {
throw new Exception('Database is not available', 500, Exception::GENERAL_SERVER_ERROR);
@@ -99,10 +97,7 @@ App::get('/v1/health/cache')
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->inject('utopia')
->action(function ($response, $utopia) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\App $utopia */
/** @var Redis */
->action(function (Response $response, App $utopia) {
$checkStart = \microtime(true);
@@ -132,8 +127,7 @@ App::get('/v1/health/time')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_TIME)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
/*
* Code from: @see https://www.beliefmedia.com.au/query-ntp-time-server
@@ -147,7 +141,7 @@ App::get('/v1/health/time')
\socket_connect($sock, $host, 123);
/* Send request */
$msg = "\010".\str_repeat("\0", 47);
$msg = "\010" . \str_repeat("\0", 47);
\socket_send($sock, $msg, \strlen($msg), 0);
@@ -190,8 +184,7 @@ App::get('/v1/health/queue/webhooks')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document([ 'size' => Resque::size(Event::WEBHOOK_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
@@ -208,30 +201,11 @@ App::get('/v1/health/queue/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document([ 'size' => Resque::size(Event::AUDITS_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/usage')
->desc('Get Usage Queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'health')
->label('sdk.method', 'getQueueUsage')
->label('sdk.description', '/docs/references/health/get-queue-usage.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
$response->dynamic(new Document([ 'size' => Resque::size(Event::USAGE_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/certificates')
->desc('Get Certificates Queue')
->groups(['api', 'health'])
@@ -244,8 +218,7 @@ App::get('/v1/health/queue/certificates')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document([ 'size' => Resque::size(Event::CERTIFICATES_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
@@ -262,8 +235,7 @@ App::get('/v1/health/queue/functions')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document([ 'size' => Resque::size(Event::FUNCTIONS_QUEUE_NAME) ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
@@ -280,25 +252,26 @@ App::get('/v1/health/storage/local')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_STATUS)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$checkStart = \microtime(true);
foreach ([
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES
] as $key => $volume) {
] as $key => $volume
) {
$device = new Local($volume);
if (!\is_readable($device->getRoot())) {
throw new Exception('Device '.$key.' dir is not readable', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception('Device ' . $key . ' dir is not readable', 500, Exception::GENERAL_SERVER_ERROR);
}
if (!\is_writable($device->getRoot())) {
throw new Exception('Device '.$key.' dir is not writable', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception('Device ' . $key . ' dir is not writable', 500, Exception::GENERAL_SERVER_ERROR);
}
}
@@ -322,8 +295,7 @@ App::get('/v1/health/anti-virus')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_ANTIVIRUS)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$output = [
'status' => '',
@@ -334,13 +306,15 @@ App::get('/v1/health/anti-virus')
$output['status'] = 'disabled';
$output['version'] = '';
} else {
$antivirus = new Network(App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310));
$antivirus = new Network(
App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
try {
$output['version'] = @$antivirus->version();
$output['status'] = (@$antivirus->ping()) ? 'pass' : 'fail';
} catch( \Exception $e) {
} catch (\Exception $e) {
throw new Exception('Antivirus is not available', 500, Exception::GENERAL_SERVER_ERROR);
}
}
@@ -359,10 +333,7 @@ App::get('/v1/health/stats') // Currently only used internally
->inject('response')
->inject('register')
->inject('deviceFiles')
->action(function ($response, $register, $deviceFiles) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Registry\Registry $register */
/** @var Utopia\Storage\Device $deviceFiles */
->action(function (Response $response, Registry $register, Device $deviceFiles) {
$cache = $register->get('cache');
@@ -371,7 +342,7 @@ App::get('/v1/health/stats') // Currently only used internally
$response
->json([
'storage' => [
'used' => Storage::human($deviceFiles->getDirectorySize($deviceFiles->getRoot().'/')),
'used' => Storage::human($deviceFiles->getDirectorySize($deviceFiles->getRoot() . '/')),
'partitionTotal' => Storage::human($deviceFiles->getPartitionTotalSpace()),
'partitionFree' => Storage::human($deviceFiles->getPartitionFreeSpace()),
],
+15 -22
View File
@@ -24,7 +24,6 @@ App::get('/v1/locale')
->inject('locale')
->inject('geodb')
->action(function (Request $request, Response $response, Locale $locale, Reader $geodb) {
$eu = Config::getParam('locale-eu');
$currencies = Config::getParam('locale-currencies');
$output = [];
@@ -39,8 +38,8 @@ App::get('/v1/locale')
if ($record) {
$output['countryCode'] = $record['country']['iso_code'];
$output['country'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
$output['continent'] = $locale->getText('continents.'.strtolower($record['continent']['code']), $locale->getText('locale.country.unknown'));
$output['country'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
$output['continent'] = $locale->getText('continents.' . strtolower($record['continent']['code']), $locale->getText('locale.country.unknown'));
$output['continent'] = (isset($continents[$record['continent']['code']])) ? $continents[$record['continent']['code']] : $locale->getText('locale.country.unknown');
$output['continentCode'] = $record['continent']['code'];
$output['eu'] = (\in_array($record['country']['iso_code'], $eu)) ? true : false;
@@ -62,8 +61,8 @@ App::get('/v1/locale')
}
$response
->addHeader('Cache-Control', 'public, max-age='.$time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
;
$response->dynamic(new Document($output), Response::MODEL_LOCALE);
});
@@ -82,13 +81,12 @@ App::get('/v1/locale/countries')
->inject('response')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = Config::getParam('locale-countries'); /* @var $list array */
$output = [];
foreach ($list as $value) {
$output[] = new Document([
'name' => $locale->getText('countries.'.strtolower($value)),
'name' => $locale->getText('countries.' . strtolower($value)),
'code' => $value,
]);
}
@@ -114,14 +112,13 @@ App::get('/v1/locale/countries/eu')
->inject('response')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$eu = Config::getParam('locale-eu');
$output = [];
foreach ($eu as $code) {
if ($locale->getText('countries.'.strtolower($code), false) !== false) {
if ($locale->getText('countries.' . strtolower($code), false) !== false) {
$output[] = new Document([
'name' => $locale->getText('countries.'.strtolower($code)),
'name' => $locale->getText('countries.' . strtolower($code)),
'code' => $code,
]);
}
@@ -148,18 +145,17 @@ App::get('/v1/locale/countries/phones')
->inject('response')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = Config::getParam('locale-phones'); /* @var $list array */
$output = [];
\asort($list);
foreach ($list as $code => $name) {
if ($locale->getText('countries.'.strtolower($code), false) !== false) {
if ($locale->getText('countries.' . strtolower($code), false) !== false) {
$output[] = new Document([
'code' => '+'.$list[$code],
'code' => '+' . $list[$code],
'countryCode' => $code,
'countryName' => $locale->getText('countries.'.strtolower($code)),
'countryName' => $locale->getText('countries.' . strtolower($code)),
]);
}
}
@@ -181,12 +177,11 @@ App::get('/v1/locale/continents')
->inject('response')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = Config::getParam('locale-continents');
$list = Config::getParam('locale-continents'); /* @var $list array */
foreach ($list as $key => $value) {
foreach ($list as $value) {
$output[] = new Document([
'name' => $locale->getText('continents.'.strtolower($value)),
'name' => $locale->getText('continents.' . strtolower($value)),
'code' => $value,
]);
}
@@ -211,10 +206,9 @@ App::get('/v1/locale/currencies')
->label('sdk.response.model', Response::MODEL_CURRENCY_LIST)
->inject('response')
->action(function (Response $response) {
$list = Config::getParam('locale-currencies');
$list = array_map(fn($node) => new Document($node), $list);
$list = array_map(fn ($node) => new Document($node), $list);
$response->dynamic(new Document(['currencies' => $list, 'total' => \count($list)]), Response::MODEL_CURRENCY_LIST);
});
@@ -233,10 +227,9 @@ App::get('/v1/locale/languages')
->label('sdk.response.model', Response::MODEL_LANGUAGE_LIST)
->inject('response')
->action(function (Response $response) {
$list = Config::getParam('locale-languages');
$list = array_map(fn ($node) => new Document($node), $list);
$response->dynamic(new Document(['languages' => $list, 'total' => \count($list)]), Response::MODEL_LANGUAGE_LIST);
});
});
+70 -128
View File
@@ -2,6 +2,9 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Validator\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\Origin;
@@ -18,16 +21,16 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Registry\Registry;
use Appwrite\Extend\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Hostname;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
App::init(function ($project) {
/** @var Utopia\Database\Document $project */
App::init(function (Document $project) {
if ($project->getId() !== 'console') {
throw new Exception('Access to this API is forbidden.', 401, Exception::GENERAL_ACCESS_FORBIDDEN);
@@ -59,10 +62,7 @@ App::post('/v1/projects')
->inject('response')
->inject('dbForConsole')
->inject('dbForProject')
->action(function ($projectId, $name, $teamId, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId, $response, $dbForConsole, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $projectId, string $name, string $teamId, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Database $dbForProject) {
$team = $dbForConsole->getDocument('teams', $teamId);
@@ -77,6 +77,9 @@ 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,
'$read' => ['team:' . $teamId],
@@ -103,7 +106,7 @@ App::post('/v1/projects')
'search' => implode(' ', [$projectId, $name]),
]));
/** @var array $collections */
$collections = Config::getParam('collections', []);
$collections = Config::getParam('collections', []);
$dbForProject->setNamespace("_{$project->getId()}");
$dbForProject->create('appwrite');
@@ -115,7 +118,7 @@ App::post('/v1/projects')
$adapter->setup();
foreach ($collections as $key => $collection) {
if(($collection['$collection'] ?? '') !== Database::METADATA) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
$attributes = [];
@@ -170,9 +173,7 @@ App::get('/v1/projects')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForConsole) {
if (!empty($cursor)) {
$cursorProject = $dbForConsole->getDocument('projects', $cursor);
@@ -210,9 +211,7 @@ App::get('/v1/projects/:projectId')
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -239,11 +238,7 @@ App::get('/v1/projects/:projectId/usage')
->inject('dbForConsole')
->inject('dbForProject')
->inject('register')
->action(function ($projectId, $range, $response, $dbForConsole, $dbForProject, $register) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Registry\Registry $register */
->action(function (string $projectId, string $range, Response $response, Database $dbForConsole, Database $dbForProject, Registry $register) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -286,7 +281,7 @@ App::get('/v1/projects/:projectId/usage')
$stats = [];
Authorization::skip(function() use ($dbForProject, $periods, $range, $metrics, &$stats) {
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
@@ -308,7 +303,7 @@ App::get('/v1/projects/:projectId/usage')
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
@@ -360,9 +355,7 @@ App::patch('/v1/projects/:projectId')
->param('legalTaxId', '', new Text(256), 'Project legal tax ID. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $name, $description, $logo, $url, $legalName, $legalCountry, $legalState, $legalCity, $legalAddress, $legalTaxId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $name, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -381,8 +374,7 @@ App::patch('/v1/projects/:projectId')
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name]))
);
->setAttribute('search', implode(' ', [$projectId, $name])));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@@ -398,14 +390,11 @@ App::patch('/v1/projects/:projectId/service')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), function ($element) {return $element['optional'];})), true), 'Service name.')
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $service, $status, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Boolean $status */
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -437,9 +426,7 @@ App::patch('/v1/projects/:projectId/oauth2')
->param('secret', '', new text(512), 'Provider secret key. Max length: 512 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $provider, $appId, $secret, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $provider, string $appId, string $secret, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -470,9 +457,7 @@ App::patch('/v1/projects/:projectId/auth/limit')
->param('limit', false, new Range(0, APP_LIMIT_USERS), 'Set the max number of users allowed in this project. Use 0 for unlimited.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $limit, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -484,8 +469,7 @@ App::patch('/v1/projects/:projectId/auth/limit')
$auths['limit'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths)
);
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@@ -505,9 +489,7 @@ App::patch('/v1/projects/:projectId/auth/:method')
->param('status', false, new Boolean(true), 'Set the status of this auth method.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $method, $status, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $method, bool $status, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
$auth = Config::getParam('auth')[$method] ?? [];
@@ -541,11 +523,7 @@ App::delete('/v1/projects/:projectId')
->inject('user')
->inject('dbForConsole')
->inject('deletes')
->action(function ($projectId, $password, $response, $user, $dbForConsole, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Appwrite\Event\Event $deletes */
->action(function (string $projectId, string $password, Response $response, Document $user, Database $dbForConsole, Delete $deletes) {
if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password
throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS);
@@ -558,8 +536,8 @@ App::delete('/v1/projects/:projectId')
}
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $project)
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($project)
;
if (!$dbForConsole->deleteDocument('teams', $project->getAttribute('teamId', null))) {
@@ -587,16 +565,14 @@ App::post('/v1/projects/:projectId/webhooks')
->label('sdk.response.model', Response::MODEL_WEBHOOK)
->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $name, $events, $url, $security, $httpUser, $httpPass, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -606,6 +582,8 @@ App::post('/v1/projects/:projectId/webhooks')
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
@@ -617,6 +595,7 @@ App::post('/v1/projects/:projectId/webhooks')
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'signatureKey' => \bin2hex(\random_bytes(64)),
]);
$webhook = $dbForConsole->createDocument('webhooks', $webhook);
@@ -640,9 +619,7 @@ App::get('/v1/projects/:projectId/webhooks')
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -674,9 +651,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $webhookId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -709,16 +684,15 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('projectId', null, new UID(), 'Project unique ID.')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->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 ($projectId, $webhookId, $name, $events, $url, $security, $httpUser, $httpPass, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $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) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -746,8 +720,11 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->setAttribute('httpPass', $httpPass)
;
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
if (!empty($signatureKey)) {
$webhook->setAttribute('signatureKey', $signatureKey);
}
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
@@ -766,9 +743,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $webhookId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -781,7 +756,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if($webhook === false || $webhook->isEmpty()) {
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
}
@@ -806,12 +781,10 @@ App::post('/v1/projects/:projectId/keys')
->label('sdk.response.model', Response::MODEL_KEY)
->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)), 'Key scopes list.')
->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.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $name, $scopes, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $name, array $scopes, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -850,9 +823,7 @@ App::get('/v1/projects/:projectId/keys')
->param('projectId', null, new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -884,9 +855,7 @@ App::get('/v1/projects/:projectId/keys/:keyId')
->param('keyId', null, new UID(), 'Key unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $keyId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -919,12 +888,10 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('projectId', null, new UID(), 'Project unique ID.')
->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)), 'Key scopes list')
->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.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $keyId, $name, $scopes, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $keyId, string $name, array $scopes, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -966,9 +933,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
->param('keyId', null, new UID(), 'Key unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $keyId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $keyId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -981,7 +946,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if($key === false || $key->isEmpty()) {
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404, Exception::KEY_NOT_FOUND);
}
@@ -1009,13 +974,10 @@ App::post('/v1/projects/:projectId/platforms')
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Text(256), 'Platform client hostname. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $type, $name, $key, $store, $hostname, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@@ -1057,9 +1019,7 @@ App::get('/v1/projects/:projectId/platforms')
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1091,9 +1051,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
->param('platformId', null, new UID(), 'Platform unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $platformId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1128,13 +1086,10 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Text(256), 'Platform client URL. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $platformId, $name, $key, $store, $hostname, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
@@ -1178,9 +1133,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
->param('platformId', null, new UID(), 'Platform unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $platformId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $platformId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1220,9 +1173,7 @@ App::post('/v1/projects/:projectId/domains')
->param('domain', null, new DomainValidator(), 'Domain name.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $domain, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $domain, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1281,9 +1232,7 @@ App::get('/v1/projects/:projectId/domains')
->param('projectId', '', new UID(), 'Project unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1315,9 +1264,7 @@ App::get('/v1/projects/:projectId/domains/:domainId')
->param('domainId', null, new UID(), 'Domain unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $domainId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1351,9 +1298,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
->param('domainId', null, new UID(), 'Domain unique ID.')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $domainId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1391,10 +1336,10 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
$dbForConsole->deleteCachedDocument('projects', $project->getId());
// Issue a TLS certificate when domain is verified
Resque::enqueue('v1-certificates', 'CertificatesV1', [
'document' => $domain->getArrayCopy(),
'domain' => $domain->getAttribute('domain'),
]);
$event = new Certificate();
$event
->setDomain($domain)
->trigger();
$response->dynamic($domain, Response::MODEL_DOMAIN);
});
@@ -1413,9 +1358,7 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
->inject('response')
->inject('dbForConsole')
->inject('deletes')
->action(function ($projectId, $domainId, $response, $dbForConsole, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
->action(function (string $projectId, string $domainId, Response $response, Database $dbForConsole, Delete $deletes) {
$project = $dbForConsole->getDocument('projects', $projectId);
@@ -1437,9 +1380,8 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$deletes
->setParam('type', DELETE_TYPE_CERTIFICATES)
->setParam('document', $domain)
;
->setType(DELETE_TYPE_CERTIFICATES)
->setDocument($domain);
$response->noContent();
});
+125 -185
View File
@@ -2,8 +2,12 @@
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Audit;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
@@ -21,6 +25,7 @@ use Utopia\Database\Validator\UID;
use Appwrite\Extend\Exception;
use Utopia\Image\Image;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
@@ -34,12 +39,13 @@ use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\Swoole\Request;
App::post('/v1/storage/buckets')
->desc('Create bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'storage.buckets.create')
->label('event', 'buckets.[bucketId].create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createBucket')
@@ -53,19 +59,16 @@ App::post('/v1/storage/buckets')
->param('read', null, new Permissions(), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($bucketId, $name, $permission, $read, $write, $enabled, $maximumFileSize, $allowedFileExtensions, $encryption, $antivirus, $response, $dbForProject, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$bucketId = $bucketId === 'unique()' ? $dbForProject->getId() : $bucketId;
try {
@@ -126,9 +129,12 @@ App::post('/v1/storage/buckets')
}
$audits
->setParam('event', 'storage.buckets.create')
->setParam('resource', 'storage/buckets/' . $bucket->getId())
->setParam('data', $bucket->getArrayCopy())
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$events
->setParam('bucketId', $bucket->getId())
;
$usage->setParam('storage.buckets.create', 1);
@@ -157,12 +163,9 @@ App::get('/v1/storage/buckets')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage) {
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, $search)] : [];
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : [];
if (!empty($cursor)) {
$cursorBucket = $dbForProject->getDocument('buckets', $cursor);
@@ -195,10 +198,7 @@ App::get('/v1/storage/buckets/:bucketId')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($bucketId, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $bucketId, Response $response, Database $dbForProject, Stats $usage) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@@ -215,7 +215,7 @@ App::put('/v1/storage/buckets/:bucketId')
->desc('Update Bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'storage.buckets.update')
->label('event', 'buckets.[bucketId].update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateBucket')
@@ -229,33 +229,29 @@ App::put('/v1/storage/buckets/:bucketId')
->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
->param('maximumFileSize', null, new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->action(function ($bucketId, $name, $permission, $read, $write, $enabled, $maximumFileSize, $allowedFileExtensions, $encryption, $antivirus, $response, $dbForProject, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
$read??=$bucket->getAttribute('$read', []); // By default inherit read permissions
$write??=$bucket->getAttribute('$write', []); // By default inherit write permissions
$maximumFileSize??=$bucket->getAttribute('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions??=$bucket->getAttribute('allowedFileExtensions', []);
$enabled??=$bucket->getAttribute('enabled', true);
$encryption??=$bucket->getAttribute('encryption', true);
$antivirus??=$bucket->getAttribute('antivirus', true);
$read ??= $bucket->getAttribute('$read', []); // By default inherit read permissions
$write ??= $bucket->getAttribute('$write', []); // By default inherit write permissions
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$bucket = $dbForProject->updateDocument('buckets', $bucket->getId(), $bucket
->setAttribute('name', $name)
@@ -266,13 +262,15 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('enabled', (bool) filter_var($enabled, FILTER_VALIDATE_BOOLEAN))
->setAttribute('encryption', (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN))
->setAttribute('permission', $permission)
->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN))
);
->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN)));
$audits
->setParam('event', 'storage.buckets.update')
->setParam('resource', 'storage/buckets/' . $bucket->getId())
->setParam('data', $bucket->getArrayCopy())
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$events
->setParam('bucketId', $bucket->getId())
;
$usage->setParam('storage.buckets.update', 1);
@@ -284,7 +282,7 @@ App::delete('/v1/storage/buckets/:bucketId')
->desc('Delete Bucket')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'storage.buckets.delete')
->label('event', 'buckets.[bucketId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteBucket')
@@ -298,14 +296,7 @@ App::delete('/v1/storage/buckets/:bucketId')
->inject('deletes')
->inject('events')
->inject('usage')
->action(function ($bucketId, $response, $dbForProject, $audits, $deletes, $events, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $bucketId, Response $response, Database $dbForProject, Audit $audits, Delete $deletes, Event $events, Stats $usage) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -317,18 +308,17 @@ App::delete('/v1/storage/buckets/:bucketId')
}
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $bucket)
;
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($bucket);
$events
->setParam('eventData', $response->output($bucket, Response::MODEL_BUCKET))
->setParam('bucketId', $bucket->getId())
->setPayload($response->output($bucket, Response::MODEL_BUCKET))
;
$audits
->setParam('event', 'storage.buckets.delete')
->setParam('resource', 'storage/buckets/' . $bucket->getId())
->setParam('data', $bucket->getArrayCopy())
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$usage->setParam('storage.buckets.delete', 1);
@@ -341,7 +331,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->desc('Create File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.create')
->label('event', 'buckets.[bucketId].files.[fileId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createFile')
@@ -366,22 +356,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('mode')
->inject('deviceFiles')
->inject('deviceLocal')
->action(function ($bucketId, $fileId, $file, $read, $write, $request, $response, $dbForProject, $user, $audits, $usage, $events, $mode, $deviceFiles, $deviceLocal) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
/** @var Utopia\Storage\Device $deviceFiles */
/** @var Utopia\Storage\Device $deviceLocal */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, array $file, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -423,7 +404,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$maximumFileSize = $bucket->getAttribute('maximumFileSize', 0);
if ($maximumFileSize > (int) App::getEnv('_APP_STORAGE_LIMIT', 0)) {
throw new Exception('Error bucket maximum file size is larger than _APP_STORAGE_LIMIT', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception('Maximum bucket file size is larger than _APP_STORAGE_LIMIT', 500, Exception::GENERAL_SERVER_ERROR);
}
$file = $request->getFiles('file');
@@ -482,7 +463,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
// Save to storage
$fileSize??=$deviceLocal->getFileSize($fileTmpName);
$fileSize ??= $deviceLocal->getFileSize($fileTmpName);
$path = $deviceFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$path = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
@@ -512,8 +493,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
$write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? [];
if ($chunksUploaded === $chunks) {
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
$antivirus = new Network(App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310));
$antivirus = new Network(
App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
if (!$antivirus->fileScan($path)) {
$deviceFiles->delete($path);
@@ -583,9 +566,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
if ($permissionBucket) {
$file = Authorization::skip(function () use ($dbForProject, $bucket, $doc) {
return $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
});
$file = Authorization::skip(fn () => $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc));
} else {
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
}
@@ -605,13 +586,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setAttribute('chunksUploaded', $chunksUploaded);
if ($permissionBucket) {
$file = Authorization::skip(function () use ($dbForProject, $bucket, $fileId, $file) {
return $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
});
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} else {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
}
} catch (StructureException $exception) {
throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE);
@@ -620,8 +598,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
$audits
->setParam('event', 'storage.files.create')
->setParam('resource', 'storage/files/' . $file->getId())
->setResource('storage/files/' . $file->getId())
;
$usage
@@ -629,7 +606,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setParam('storage.files.create', 1)
->setParam('bucketId', $bucketId)
;
} else {
try {
if ($file->isEmpty()) {
@@ -653,9 +629,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
if ($permissionBucket) {
$file = Authorization::skip(function () use ($dbForProject, $bucket, $doc) {
return $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
});
$file = Authorization::skip(fn () => $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc));
} else {
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
}
@@ -665,9 +639,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setAttribute('metadata', $metadata);
if ($permissionBucket) {
$file = Authorization::skip(function () use ($dbForProject, $bucket, $fileId, $file) {
return $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
});
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} else {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
@@ -679,13 +651,16 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
}
$events->setParam('bucket', $bucket->getArrayCopy());
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
;
$metadata = null; // was causing leaks as it was passed by reference
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($file, Response::MODEL_FILE);
;
});
App::get('/v1/storage/buckets/:bucketId/files')
@@ -711,16 +686,14 @@ App::get('/v1/storage/buckets/:bucketId/files')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->action(function ($bucketId, $search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject, $usage, $mode) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var string $mode */
->action(function (string $bucketId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -791,16 +764,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('dbForProject')
->inject('usage')
->inject('mode')
->action(function ($bucketId, $fileId, $response, $dbForProject, $usage, $mode) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -863,15 +834,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('mode')
->inject('deviceFiles')
->inject('deviceLocal')
->action(function ($bucketId, $fileId, $width, $height, $gravity, $quality, $borderWidth, $borderColor, $borderRadius, $opacity, $rotation, $background, $output, $request, $response, $project, $dbForProject, $usage, $mode, $deviceFiles, $deviceLocal) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Utopia\Storage\Device $deviceFiles */
/** @var Utopia\Storage\Device $deviceLocal */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles, Device $deviceLocal) {
if (!\extension_loaded('imagick')) {
throw new Exception('Imagick extension is missing', 500, Exception::GENERAL_SERVER_ERROR);
@@ -879,8 +842,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -921,7 +886,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$mime = $file->getAttribute('mimeType');
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
if(!\in_array($mime, $inputs)) {
if (!\in_array($mime, $inputs)) {
$path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default'];
} else {
// it was an image but the file size exceeded the limit
@@ -946,7 +911,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId)); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if(empty($output)) {
if (empty($output)) {
// when file extension is not provided and the mime type is not one of our supported outputs
// we fallback to `jpg` output format
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
@@ -1041,18 +1006,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->action(function ($bucketId, $fileId, $request, $response, $dbForProject, $usage, $mode, $deviceFiles) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
/** @var Utopia\Storage\Device $deviceFiles */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -1184,18 +1145,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->action(function ($bucketId, $fileId, $response, $request, $dbForProject, $usage, $mode, $deviceFiles) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Swoole\Request $request */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
/** @var Utopia\Storage\Device $deviceFiles */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, Stats $usage, string $mode, Device $deviceFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -1325,7 +1282,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Update File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.update')
->label('event', 'buckets.[bucketId].files.[fileId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateFile')
@@ -1344,15 +1301,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('usage')
->inject('mode')
->inject('events')
->action(function ($bucketId, $fileId, $read, $write, $response, $dbForProject, $user, $audits, $usage, $mode, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, ?array $read, ?array $write, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, string $mode, Event $events) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user
$write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? [];
@@ -1373,8 +1322,10 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
}
}
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -1407,13 +1358,14 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
$events->setParam('bucket', $bucket->getArrayCopy());
$audits
->setParam('event', 'storage.files.update')
->setParam('resource', 'file/' . $file->getId())
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
;
$audits->setResource('file/' . $file->getId());
$usage
->setParam('storage.files.update', 1)
->setParam('bucketId', $bucketId)
@@ -1427,7 +1379,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Delete File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.delete')
->label('event', 'buckets.[bucketId].files.[fileId].delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteFile')
@@ -1444,20 +1396,13 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('mode')
->inject('deviceFiles')
->inject('project')
->action(function ($bucketId, $fileId, $response, $dbForProject, $events, $audits, $usage, $mode, $deviceFiles, $project) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Utopia\Storage\Device $deviceFiles */
/** @var string $mode */
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Audit $audits, Stats $usage, string $mode, Device $deviceFiles, Document $project) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -1507,10 +1452,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
throw new Exception('Failed to delete file from device', 500, Exception::GENERAL_SERVER_ERROR);
}
$audits
->setParam('event', 'storage.files.delete')
->setParam('resource', 'file/' . $file->getId())
;
$audits->setResource('file/' . $file->getId());
$usage
->setParam('storage', $file->getAttribute('size', 0) * -1)
@@ -1519,8 +1461,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
;
$events
->setParam('eventData', $response->output($file, Response::MODEL_FILE))
->setParam('bucket', $bucket->getArrayCopy())
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext($bucket)
->setPayload($response->output($file, Response::MODEL_FILE))
;
$response->dynamic(new Document(), Response::MODEL_NONE);
@@ -1539,9 +1483,7 @@ App::get('/v1/storage/usage')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function ($range, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $range, Response $response, Database $dbForProject) {
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
@@ -1600,10 +1542,10 @@ App::get('/v1/storage/usage')
}
// backfill metrics with empty values for graphs
$backfill = $limit-\count($requestDocs);
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
@@ -1651,9 +1593,7 @@ App::get('/v1/storage/:bucketId/usage')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->action(function ($bucketId, $range, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@@ -1711,10 +1651,10 @@ App::get('/v1/storage/:bucketId/usage')
}
// backfill metrics with empty values for graphs
$backfill = $limit-\count($requestDocs);
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
+236 -178
View File
@@ -2,13 +2,20 @@
use Appwrite\Auth\Auth;
use Appwrite\Detector\Detector;
use Appwrite\Event\Audit as EventAudit;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -18,6 +25,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
@@ -26,7 +34,7 @@ use Utopia\Validator\WhiteList;
App::post('/v1/teams')
->desc('Create Team')
->groups(['api', 'teams'])
->label('event', 'teams.create')
->label('event', 'teams.[teamId].create')
->label('scope', 'teams.write')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -37,16 +45,13 @@ App::post('/v1/teams')
->label('sdk.response.model', Response::MODEL_TEAM)
->param('teamId', '', new CustomId(), 'Team 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('name', null, new Text(128), 'Team name. Max length: 128 chars.')
->param('roles', ['owner'], new ArrayList(new Key()), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.', true)
->param('roles', ['owner'], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', true)
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function ($teamId, $name, $roles, $response, $user, $dbForProject, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
->inject('audits')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events, Event $audits) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
@@ -54,8 +59,8 @@ App::post('/v1/teams')
$teamId = $teamId == 'unique()' ? $dbForProject->getId() : $teamId;
$team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId ,
'$read' => ['team:'.$teamId],
'$write' => ['team:'.$teamId .'/owner'],
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner'],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'dateCreated' => \time(),
@@ -66,8 +71,8 @@ App::post('/v1/teams')
$membershipId = $dbForProject->getId();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['user:'.$user->getId(), 'team:'.$team->getId()],
'$write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'],
'$read' => ['user:' . $user->getId(), 'team:' . $team->getId()],
'$write' => ['user:' . $user->getId(), 'team:' . $team->getId() . '/owner'],
'userId' => $user->getId(),
'teamId' => $team->getId(),
'roles' => $roles,
@@ -79,16 +84,21 @@ App::post('/v1/teams')
]);
$membership = $dbForProject->createDocument('memberships', $membership);
// Attach user to team
$user->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->deleteCachedDocument('users', $user->getId());
}
$events->setParam('teamId', $team->getId());
if (!empty($user->getId())) {
$events->setParam('userId', $user->getId());
}
$audits
->setParam('event', 'teams.create')
->setParam('resource', 'team/' . $teamId)
->setParam('data', $team->getArrayCopy())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($team, Response::MODEL_TEAM);
});
@@ -112,9 +122,7 @@ App::get('/v1/teams')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
if (!empty($cursor)) {
$cursorTeam = $dbForProject->getDocument('teams', $cursor);
@@ -153,9 +161,7 @@ App::get('/v1/teams/:teamId')
->param('teamId', '', new UID(), 'Team ID.')
->inject('response')
->inject('dbForProject')
->action(function ($teamId, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $teamId, Response $response, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -169,7 +175,7 @@ App::get('/v1/teams/:teamId')
App::put('/v1/teams/:teamId')
->desc('Update Team')
->groups(['api', 'teams'])
->label('event', 'teams.update')
->label('event', 'teams.[teamId].update')
->label('scope', 'teams.write')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -182,9 +188,9 @@ App::put('/v1/teams/:teamId')
->param('name', null, new Text(128), 'New team name. Max length: 128 chars.')
->inject('response')
->inject('dbForProject')
->action(function ($teamId, $name, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->inject('events')
->inject('audits')
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events, EventAudit $audits) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -192,10 +198,12 @@ App::put('/v1/teams/:teamId')
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
}
$team = $dbForProject->updateDocument('teams', $team->getId(),$team
$team = $dbForProject->updateDocument('teams', $team->getId(), $team
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$teamId, $name]))
);
->setAttribute('search', implode(' ', [$teamId, $name])));
$events->setParam('teamId', $team->getId());
$audits->setResource('team/' . $team->getId());
$response->dynamic($team, Response::MODEL_TEAM);
});
@@ -203,7 +211,7 @@ App::put('/v1/teams/:teamId')
App::delete('/v1/teams/:teamId')
->desc('Delete Team')
->groups(['api', 'teams'])
->label('event', 'teams.delete')
->label('event', 'teams.[teamId].delete')
->label('scope', 'teams.write')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -216,11 +224,8 @@ App::delete('/v1/teams/:teamId')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->action(function ($teamId, $response, $dbForProject, $events, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $deletes */
->inject('audits')
->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes, EventAudit $audits) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -244,12 +249,18 @@ App::delete('/v1/teams/:teamId')
}
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $team)
;
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($team);
$events
->setParam('eventData', $response->output($team, Response::MODEL_TEAM))
->setParam('teamId', $team->getId())
->setPayload($response->output($team, Response::MODEL_TEAM))
;
$audits
->setParam('event', 'teams.delete')
->setParam('resource', 'team/' . $teamId)
->setParam('data', $team->getArrayCopy())
;
$response->dynamic(new Document(), Response::MODEL_NONE);
@@ -258,7 +269,7 @@ App::delete('/v1/teams/:teamId')
App::post('/v1/teams/:teamId/memberships')
->desc('Create Team Membership')
->groups(['api', 'teams', 'auth'])
->label('event', 'teams.memberships.create')
->label('event', 'teams.[teamId].memberships.[membershipId].create')
->label('scope', 'teams.write')
->label('auth.type', 'invites')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
@@ -271,8 +282,8 @@ App::post('/v1/teams/:teamId/memberships')
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.')
->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')
@@ -281,21 +292,16 @@ App::post('/v1/teams/:teamId/memberships')
->inject('locale')
->inject('audits')
->inject('mails')
->action(function ($teamId, $email, $roles, $url, $name, $response, $project, $user, $dbForProject, $locale, $audits, $mails) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $mails */
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
->inject('events')
->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, EventAudit $audits, Mail $mails, Event $events) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
if (!$isPrivilegedUser && !$isAppUser && empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
$email = \strtolower($email);
$name = (empty($name)) ? $email : $name;
$team = $dbForProject->getDocument('teams', $teamId);
@@ -307,13 +313,12 @@ App::post('/v1/teams/:teamId/memberships')
$invitee = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if (empty($invitee)) { // Create new user if no user with same email found
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed.
$total = $dbForProject->count('users', [], APP_LIMIT_USERS);
if($total >= $limit) {
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
}
}
@@ -322,33 +327,33 @@ App::post('/v1/teams/:teamId/memberships')
$userId = $dbForProject->getId();
$invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['user:'.$userId, 'role:all'],
'$write' => ['user:'.$userId],
'$read' => ['user:' . $userId, 'role:all'],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash(Auth::passwordGenerator()),
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
])));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS);
}
}
$isOwner = Authorization::isRole('team:'.$team->getId().'/owner');;
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception('User is not allowed to send invitations for this team', 401, Exception::USER_UNAUTHORIZED);
@@ -360,7 +365,7 @@ App::post('/v1/teams/:teamId/memberships')
$membership = new Document([
'$id' => $membershipId,
'$read' => ['role:all'],
'$write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'],
'$write' => ['user:' . $invitee->getId(), 'team:' . $team->getId() . '/owner'],
'userId' => $invitee->getId(),
'teamId' => $team->getId(),
'roles' => $roles,
@@ -375,15 +380,12 @@ App::post('/v1/teams/:teamId/memberships')
try {
$membership = Authorization::skip(fn() => $dbForProject->createDocument('memberships', $membership));
} catch (Duplicate $th) {
throw new Exception('User has already been invited or is already a member of this team', 409, Exception::TEAM_INVITE_ALREADY_EXISTS);
throw new Exception('User is already a member of this team', 409, Exception::TEAM_INVITE_ALREADY_EXISTS);
}
$team->setAttribute('total', $team->getAttribute('total', 0) + 1);
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
// Attach user to team
$invitee->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND);
$invitee = Authorization::skip(fn() => $dbForProject->updateDocument('users', $invitee->getId(), $invitee));
$dbForProject->deleteCachedDocument('users', $invitee->getId());
} else {
try {
$membership = $dbForProject->createDocument('memberships', $membership);
@@ -398,31 +400,34 @@ App::post('/v1/teams/:teamId/memberships')
if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode
$mails
->setParam('event', 'teams.memberships.create')
->setParam('from', $project->getId())
->setParam('recipient', $email)
->setParam('name', $name)
->setParam('url', $url)
->setParam('locale', $locale->default)
->setParam('project', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('owner', $user->getAttribute('name', ''))
->setParam('team', $team->getAttribute('name', '[TEAM-NAME]'))
->setParam('type', MAIL_TYPE_INVITATION)
->setType(MAIL_TYPE_INVITATION)
->setRecipient($email)
->setUrl($url)
->setName($name)
->setLocale($locale->default)
->setTeam($team)
->setUser($user)
->trigger()
;
}
$audits
->setParam('userId', $invitee->getId())
->setParam('event', 'teams.memberships.create')
->setParam('resource', 'team/'.$teamId)
->setResource('team/' . $teamId)
;
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($membership
->setAttribute('email', $email)
->setAttribute('name', $name)
, Response::MODEL_MEMBERSHIP);
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
App::get('/v1/teams/:teamId/memberships')
@@ -445,9 +450,7 @@ App::get('/v1/teams/:teamId/memberships')
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function ($teamId, $search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $teamId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -487,12 +490,13 @@ App::get('/v1/teams/:teamId/memberships')
$memberships = array_filter($memberships, fn(Document $membership) => !empty($membership->getAttribute('userId')));
$memberships = array_map(function($membership) use ($dbForProject) {
$memberships = array_map(function ($membership) use ($dbForProject, $team) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$membership
->setAttribute('name', $user->getAttribute('name'))
->setAttribute('email', $user->getAttribute('email'))
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'))
;
return $membership;
@@ -519,9 +523,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('dbForProject')
->action(function ($teamId, $membershipId, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -531,24 +533,25 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
$membership = $dbForProject->getDocument('memberships', $membershipId);
if($membership->isEmpty() || empty($membership->getAttribute('userId'))) {
if ($membership->isEmpty() || empty($membership->getAttribute('userId'))) {
throw new Exception('Membership not found', 404, Exception::MEMBERSHIP_NOT_FOUND);
}
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$membership
->setAttribute('name', $user->getAttribute('name'))
->setAttribute('email', $user->getAttribute('email'))
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'))
;
$response->dynamic($membership, Response::MODEL_MEMBERSHIP );
$response->dynamic($membership, Response::MODEL_MEMBERSHIP);
});
App::patch('/v1/teams/:teamId/memberships/:membershipId')
->desc('Update Membership Roles')
->groups(['api', 'teams'])
->label('event', 'teams.memberships.update')
->label('event', 'teams.[teamId].memberships.[membershipId].update')
->label('scope', 'teams.write')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -559,18 +562,14 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->param('teamId', '', new UID(), 'Team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->param('roles', [], new ArrayList(new Key()), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Max length for each role is 32 chars.')
->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('audits')
->action(function ($teamId, $membershipId, $roles, $request, $response, $user, $dbForProject, $audits) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
->inject('events')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, EventAudit $audits, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -589,7 +588,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');;
$isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception('User is not allowed to modify roles', 401, Exception::USER_UNAUTHORIZED);
@@ -604,23 +603,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
/**
* Replace membership on profile
*/
$memberships = array_filter($profile->getAttribute('memberships'), fn (Document $m) => $m->getId() !== $membership->getId());
$dbForProject->deleteCachedDocument('users', $profile->getId());
$profile
->setAttribute('memberships', $memberships)
->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND);
$audits->setResource('team/' . $teamId);
Authorization::skip(fn () => $dbForProject->updateDocument('users', $profile->getId(), $profile));
$audits
->setParam('userId', $user->getId())
->setParam('event', 'teams.memberships.update')
->setParam('resource', 'team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId());
$response->dynamic(
$membership
->setAttribute('email', $profile->getAttribute('email'))
->setAttribute('name', $profile->getAttribute('name')),
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $profile->getAttribute('name'))
->setAttribute('userEmail', $profile->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
@@ -628,7 +623,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->desc('Update Team Membership Status')
->groups(['api', 'teams'])
->label('event', 'teams.memberships.update.status')
->label('event', 'teams.[teamId].memberships.[membershipId].update.status')
->label('scope', 'public')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -647,14 +642,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('dbForProject')
->inject('geodb')
->inject('audits')
->action(function ($teamId, $membershipId, $userId, $secret, $request, $response, $user, $dbForProject, $geodb, $audits) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
->inject('events')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, EventAudit $audits, Event $events) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -677,8 +666,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception('Secret key not valid', 401, Exception::TEAM_INVALID_SECRET);
}
if ($userId != $membership->getAttribute('userId')) {
throw new Exception('Invite does not belong to current user ('.$user->getAttribute('email').')', 401, Exception::TEAM_INVITE_MISMATCH);
if ($userId !== $membership->getAttribute('userId')) {
throw new Exception('Invite does not belong to current user (' . $user->getAttribute('email') . ')', 401, Exception::TEAM_INVITE_MISMATCH);
}
if ($user->isEmpty()) {
@@ -686,7 +675,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
}
if ($membership->getAttribute('userId') !== $user->getId()) {
throw new Exception('Invite does not belong to current user ('.$user->getAttribute('email').')', 401, Exception::TEAM_INVITE_MISMATCH);
throw new Exception('Invite does not belong to current user (' . $user->getAttribute('email') . ')', 401, Exception::TEAM_INVITE_MISMATCH);
}
if ($membership->getAttribute('confirm') === true) {
@@ -700,12 +689,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$user
->setAttribute('emailVerification', true)
->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND)
;
// Log user in
Authorization::setRole('user:'.$user->getId());
Authorization::setRole('user:' . $user->getId());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@@ -724,23 +712,24 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:'.$user->getId()])
->setAttribute('$write', ['user:'.$user->getId()])
);
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
$dbForProject->deleteCachedDocument('users', $user->getId());
Authorization::setRole('user:'.$userId);
Authorization::setRole('user:' . $userId);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
$dbForProject->deleteCachedDocument('users', $user->getId());
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1)));
$audits
->setParam('userId', $user->getId())
->setParam('event', 'teams.memberships.update.status')
->setParam('resource', 'team/'.$teamId)
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
;
if (!Config::getParam('domainVerification')) {
@@ -750,20 +739,23 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
}
$response
->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->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'))
;
$response->dynamic($membership
->setAttribute('email', $user->getAttribute('email'))
->setAttribute('name', $user->getAttribute('name'))
, Response::MODEL_MEMBERSHIP);
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
App::delete('/v1/teams/:teamId/memberships/:membershipId')
->desc('Delete Team Membership')
->groups(['api', 'teams'])
->label('event', 'teams.memberships.delete')
->label('event', 'teams.[teamId].memberships.[membershipId].delete')
->label('scope', 'teams.write')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
@@ -777,11 +769,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function ($teamId, $membershipId, $response, $dbForProject, $audits, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
@@ -813,35 +801,105 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception('Failed to remove membership from DB', 500, Exception::GENERAL_SERVER_ERROR);
}
$memberships = $user->getAttribute('memberships', []);
foreach ($memberships as $key => $child) {
/** @var Document $child */
if ($membershipId == $child->getId()) {
unset($memberships[$key]);
break;
}
}
$user->setAttribute('memberships', $memberships);
Authorization::skip(fn() => $dbForProject->updateDocument('users', $user->getId(), $user));
$dbForProject->deleteCachedDocument('users', $user->getId());
if ($membership->getAttribute('confirm')) { // Count only confirmed members
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0));
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
}
$audits
->setParam('userId', $membership->getAttribute('userId'))
->setParam('event', 'teams.memberships.delete')
->setParam('resource', 'team/'.$teamId)
;
$audits->setResource('team/' . $teamId);
$events
->setParam('eventData', $response->output($membership, Response::MODEL_MEMBERSHIP))
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
->setPayload($response->output($membership, Response::MODEL_MEMBERSHIP))
;
$response->dynamic(new Document(), Response::MODEL_NONE);
});
App::get('/v1/teams/:teamId/logs')
->desc('List Team Logs')
->groups(['api', 'teams'])
->label('scope', 'teams.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'listLogs')
->label('sdk.description', '/docs/references/teams/get-team-logs.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('teamId', null, new UID(), 'Team ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs 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)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->action(function ($teamId, $limit, $offset, $response, $dbForProject, $locale, $geodb) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
}
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+178 -182
View File
@@ -3,12 +3,17 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Audit as EventAudit;
use Appwrite\Network\Validator\Email;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Locale\Locale;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
@@ -21,11 +26,12 @@ use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use MaxMind\Db\Reader;
App::post('/v1/users')
->desc('Create User')
->groups(['api', 'users'])
->label('event', 'users.create')
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -41,10 +47,8 @@ App::post('/v1/users')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $email, $password, $name, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$email = \strtolower($email);
@@ -53,7 +57,7 @@ App::post('/v1/users')
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:'.$userId],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'status' => true,
@@ -63,11 +67,10 @@ App::post('/v1/users')
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
]));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS);
@@ -77,6 +80,10 @@ App::post('/v1/users')
->setParam('users.create', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
@@ -101,10 +108,7 @@ App::get('/v1/users')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($search, $limit, $offset, $cursor, $cursorDirection, $orderType, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject, Stats $usage) {
if (!empty($cursor)) {
$cursorUser = $dbForProject->getDocument('users', $cursor);
@@ -114,9 +118,7 @@ App::get('/v1/users')
}
}
$queries = [
new Query('deleted', Query::TYPE_EQUAL, [false])
];
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
@@ -147,14 +149,11 @@ App::get('/v1/users/:userId')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, Response $response, Database $dbForProject, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -179,14 +178,11 @@ App::get('/v1/users/:userId/prefs')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, Response $response, Database $dbForProject, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -214,24 +210,20 @@ App::get('/v1/users/:userId/sessions')
->inject('dbForProject')
->inject('locale')
->inject('usage')
->action(function ($userId, $response, $dbForProject, $locale, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, Response $response, Database $dbForProject, Locale $locale, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {
foreach ($sessions as $key => $session) {
/** @var Document $session */
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', false);
@@ -247,6 +239,45 @@ App::get('/v1/users/:userId/sessions')
]), Response::MODEL_SESSION_LIST);
});
App::get('/v1/users/:userId/memberships')
->desc('Get User Memberships')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getMemberships')
->label('sdk.description', '/docs/references/users/get-user-memberships.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$memberships = array_map(function ($membership) use ($dbForProject, $user) {
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'));
return $membership;
}, $user->getAttribute('memberships', []));
$response->dynamic(new Document([
'memberships' => $memberships,
'total' => count($memberships),
]), Response::MODEL_MEMBERSHIP_LIST);
});
App::get('/v1/users/:userId/logs')
->desc('Get User Logs')
->groups(['api', 'users'])
@@ -266,41 +297,17 @@ App::get('/v1/users/:userId/logs')
->inject('locale')
->inject('geodb')
->inject('usage')
->action(function ($userId, $limit, $offset, $response, $dbForProject, $locale, $geodb, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, int $limit, int $offset, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$audit = new Audit($dbForProject);
$auditEvents = [
'account.create',
'account.delete',
'account.update.name',
'account.update.email',
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',
'account.verification.create',
'account.verification.update',
'teams.membership.create',
'teams.membership.update',
'teams.membership.delete',
];
$logs = $audit->getLogsByUserAndEvents($user->getId(), $auditEvents, $limit, $offset);
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
$output = [];
@@ -335,8 +342,8 @@ App::get('/v1/users/:userId/logs')
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
@@ -348,7 +355,7 @@ App::get('/v1/users/:userId/logs')
;
$response->dynamic(new Document([
'total' => $audit->countLogsByUserAndEvents($user->getId(), $auditEvents),
'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -356,7 +363,7 @@ App::get('/v1/users/:userId/logs')
App::patch('/v1/users/:userId/status')
->desc('Update User Status')
->groups(['api', 'users'])
->label('event', 'users.update.status')
->label('event', 'users.[userId].update.status')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -370,14 +377,12 @@ App::patch('/v1/users/:userId/status')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $status, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $userId, bool $status, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -386,13 +391,18 @@ App::patch('/v1/users/:userId/status')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/verification')
->desc('Update Email Verification')
->groups(['api', 'users'])
->label('event', 'users.update.verification')
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -406,14 +416,12 @@ App::patch('/v1/users/:userId/verification')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $emailVerification, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -422,13 +430,18 @@ App::patch('/v1/users/:userId/verification')
$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'])
->label('event', 'users.update.name')
->label('event', 'users.[userId].update.name')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -442,23 +455,28 @@ App::patch('/v1/users/:userId/name')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $name, $response, $dbForProject, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
->inject('events')
->action(function (string $userId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('name', $name));
$user
->setAttribute('name', $name)
->setAttribute('search', \implode(' ', [$user->getId(), $user->getAttribute('email'), $name]));
;
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.name')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@@ -467,7 +485,7 @@ App::patch('/v1/users/:userId/name')
App::patch('/v1/users/:userId/password')
->desc('Update Password')
->groups(['api', 'users'])
->label('event', 'users.update.password')
->label('event', 'users.[userId].update.password')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -481,14 +499,12 @@ App::patch('/v1/users/:userId/password')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $password, $response, $dbForProject, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
->inject('events')
->action(function (string $userId, string $password, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -499,9 +515,11 @@ App::patch('/v1/users/:userId/password')
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.password')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@@ -510,7 +528,7 @@ App::patch('/v1/users/:userId/password')
App::patch('/v1/users/:userId/email')
->desc('Update Email')
->groups(['api', 'users'])
->label('event', 'users.update.email')
->label('event', 'users.[userId].update.email')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -524,14 +542,12 @@ App::patch('/v1/users/:userId/email')
->inject('response')
->inject('dbForProject')
->inject('audits')
->action(function ($userId, $email, $response, $dbForProject, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $audits */
->inject('events')
->action(function (string $userId, string $email, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -542,16 +558,24 @@ App::patch('/v1/users/:userId/email')
$email = \strtolower($email);
$user
->setAttribute('email', $email)
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name')]))
;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('email', $email));
} catch(Duplicate $th) {
$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())
;
$events
->setParam('userId', $user->getId())
->setParam('event', 'users.update.email')
->setParam('resource', 'user/'.$user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
@@ -560,7 +584,7 @@ App::patch('/v1/users/:userId/email')
App::patch('/v1/users/:userId/prefs')
->desc('Update User Preferences')
->groups(['api', 'users'])
->label('event', 'users.update.prefs')
->label('event', 'users.[userId].update.prefs')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -574,14 +598,12 @@ App::patch('/v1/users/:userId/prefs')
->inject('response')
->inject('dbForProject')
->inject('usage')
->action(function ($userId, $prefs, $response, $dbForProject, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Stats\Stats $usage */
->inject('events')
->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -590,13 +612,18 @@ App::patch('/v1/users/:userId/prefs')
$usage
->setParam('users.update', 1)
;
$events
->setParam('userId', $user->getId())
;
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete User Session')
->groups(['api', 'users'])
->label('event', 'users.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -610,49 +637,41 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->inject('dbForProject')
->inject('events')
->inject('usage')
->action(function ($userId, $sessionId, $response, $dbForProject, $events, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, string $sessionId, Response $response, Database $dbForProject, Event $events, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$sessions = $user->getAttribute('sessions', []);
$session = $dbForProject->getDocument('sessions', $sessionId);
foreach ($sessions as $key => $session) { /** @var Document $session */
if ($sessionId == $session->getId()) {
unset($sessions[$key]);
$dbForProject->deleteDocument('sessions', $session->getId());
$user->setAttribute('sessions', $sessions);
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
$dbForProject->updateDocument('users', $user->getId(), $user);
}
if ($session->isEmpty()) {
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
}
$dbForProject->deleteDocument('sessions', $session->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $sessionId)
;
$response->noContent();
});
App::delete('/v1/users/:userId/sessions')
->desc('Delete User Sessions')
->groups(['api', 'users'])
->label('event', 'users.sessions.delete')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -665,15 +684,11 @@ App::delete('/v1/users/:userId/sessions')
->inject('dbForProject')
->inject('events')
->inject('usage')
->action(function ($userId, $response, $dbForProject, $events, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, Response $response, Database $dbForProject, Event $events, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
@@ -681,25 +696,28 @@ App::delete('/v1/users/:userId/sessions')
foreach ($sessions as $key => $session) { /** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
//TODO: fix this
}
$dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', []));
$dbForProject->deleteCachedDocument('users', $user->getId());
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_USER))
;
$usage
->setParam('users.update', 1)
->setParam('users.sessions.delete', 1)
;
$response->noContent();
});
App::delete('/v1/users/:userId')
->desc('Delete User')
->groups(['api', 'users'])
->label('event', 'users.delete')
->label('event', 'users.[userId].delete')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
@@ -707,51 +725,33 @@ App::delete('/v1/users/:userId')
->label('sdk.description', '/docs/references/users/delete.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('userId', '', function () {return new UID();}, 'User ID.')
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('usage')
->action(function ($userId, $response, $dbForProject, $events, $deletes, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Stats\Stats $usage */
->action(function (string $userId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty() || $user->getAttribute('deleted')) {
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
/**
* DO NOT DELETE THE USER RECORD ITSELF.
* WE RETAIN THE USER RECORD TO RESERVE THE USER ID AND ENSURE THAT THE USER ID IS NOT REUSED.
*/
// clone user object to send to workers
$clone = clone $user;
$user
->setAttribute("name", null)
->setAttribute("email", null)
->setAttribute("password", null)
->setAttribute("deleted", true)
->setAttribute("tokens", [])
->setAttribute("search", null)
;
$dbForProject->updateDocument('users', $userId, $user);
$dbForProject->deleteDocument('users', $userId);
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $clone)
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone)
;
$events
->setParam('eventData', $response->output($clone, Response::MODEL_USER))
->setParam('userId', $user->getId())
->setPayload($response->output($clone, Response::MODEL_USER))
;
$usage
@@ -772,13 +772,11 @@ App::get('/v1/users/usage')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn($value) => "oauth-".$value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
->action(function ($range, $provider, $response, $dbForProject) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
->action(function (string $range, string $provider, Response $response, Database $dbForProject) {
$usage = [];
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
@@ -814,7 +812,7 @@ App::get('/v1/users/usage')
$stats = [];
Authorization::skip(function() use ($dbForProject, $periods, $range, $metrics, &$stats) {
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
foreach ($metrics as $metric) {
$limit = $periods[$range]['limit'];
$period = $periods[$range]['period'];
@@ -823,7 +821,7 @@ App::get('/v1/users/usage')
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $limit, 0, ['time'], [Database::ORDER_DESC]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
$stats[$metric][] = [
@@ -835,9 +833,8 @@ App::get('/v1/users/usage')
// backfill metrics with empty values for graphs
$backfill = $limit - \count($requestDocs);
while ($backfill > 0) {
$last = $limit - $backfill - 1; // array index of last added metric
$diff = match($period) { // convert period to seconds for unix timestamp math
$diff = match ($period) { // convert period to seconds for unix timestamp math
'30m' => 1800,
'1d' => 86400,
};
@@ -848,7 +845,7 @@ App::get('/v1/users/usage')
$backfill--;
}
$stats[$metric] = array_reverse($stats[$metric]);
}
}
});
$usage = new Document([
@@ -862,8 +859,7 @@ App::get('/v1/users/usage')
'sessionsProviderCreate' => $stats["users.sessions.$provider.create"],
'sessionsDelete' => $stats["users.sessions.delete"]
]);
}
$response->dynamic($usage, Response::MODEL_USAGE_USERS);
});
});
+140 -122
View File
@@ -1,42 +1,40 @@
<?php
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\View;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
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 Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Hostname;
use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
App::init(function ($utopia, $request, $response, $console, $project, $dbForConsole, $user, $locale, $clients) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $console */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
/** @var array $clients */
App::init(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) {
/*
* Request format
@@ -46,13 +44,16 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($requestFormat) {
switch($requestFormat) {
case version_compare ($requestFormat , '0.12.0', '<') :
switch ($requestFormat) {
case version_compare($requestFormat, '0.12.0', '<'):
Request::setFilter(new RequestV12());
break;
case version_compare ($requestFormat , '0.13.0', '<') :
case version_compare($requestFormat, '0.13.0', '<'):
Request::setFilter(new RequestV13());
break;
case version_compare($requestFormat, '0.14.0', '<'):
Request::setFilter(new RequestV14());
break;
default:
Request::setFilter(null);
}
@@ -68,36 +69,45 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$domains[$domain->get()] = false;
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
} elseif(str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
} else {
Authorization::disable();
$domainDocument = $dbForConsole->findOne('domains', [
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
]);
if (!$domainDocument) {
$domainDocument = new Document([
'domain' => $domain->get(),
'tld' => $domain->getSuffix(),
'registerable' => $domain->getRegisterable(),
'verification' => false,
'certificateId' => null,
]);
$domainDocument = $dbForConsole->createDocument('domains', $domainDocument);
Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in a few seconds...');
Resque::enqueue('v1-certificates', 'CertificatesV1', [
'document' => $domainDocument,
'domain' => $domain->get(),
'validateTarget' => false,
'validateCNAME' => false,
]);
$envDomain = App::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
$domainDocument = $dbForConsole->findOne('domains', [
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
]);
if (!$domainDocument) {
$domainDocument = new Document([
'domain' => $domain->get(),
'tld' => $domain->getSuffix(),
'registerable' => $domain->getRegisterable(),
'verification' => false,
'certificateId' => null,
]);
$domainDocument = $dbForConsole->createDocument('domains', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
(new Certificate())
->setDomain($domainDocument)
->trigger();
}
}
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
@@ -111,11 +121,11 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
}
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
}
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
throw new Exception('Missing or unknown project ID', 400, Exception::PROJECT_UNKNOWN);
throw new AppwriteException('Missing or unknown project ID', 400, AppwriteException::PROJECT_UNKNOWN);
}
$referrer = $request->getReferer();
@@ -123,41 +133,50 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT);
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()).'://'.((\in_array($origin, $clients))
? $origin : 'localhost').(!empty($port) ? ':'.$port : '');
$refDomainOrigin = 'localhost';
$validator = new Hostname($clients);
if ($validator->isValid($origin)) {
$refDomainOrigin = $origin;
}
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : '');
$refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible
? $refDomain
: (!empty($protocol) ? $protocol : $request->getProtocol()).'://'.$origin.(!empty($port) ? ':'.$port : '');
: (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam('domainVerification',
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== '');
$endDomain->getRegisterable() !== ''
);
Config::setParam('cookieDomain', (
$request->getHostname() === 'localhost' ||
$request->getHostname() === 'localhost:'.$request->getPort() ||
$request->getHostname() === 'localhost:' . $request->getPort() ||
(\filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false)
)
? null
: '.'.$request->getHostname()
);
: '.' . $request->getHostname());
/*
/*
* Response format
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
switch($responseFormat) {
case version_compare ($responseFormat , '0.11.2', '<=') :
switch ($responseFormat) {
case version_compare($responseFormat, '0.11.2', '<='):
Response::setFilter(new ResponseV11());
break;
case version_compare ($responseFormat , '0.12.4', '<='):
case version_compare($responseFormat, '0.12.4', '<='):
Response::setFilter(new ResponseV12());
break;
case version_compare($responseFormat, '0.13.4', '<='):
Response::setFilter(new ResponseV13());
break;
default:
Response::setFilter(null);
}
@@ -173,10 +192,14 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
*/
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https') {
return $response->redirect('https://'.$request->getHostname().$request->getURI());
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException('Method unsupported over HTTP.', 500, AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED);
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
$response->addHeader('Strict-Transport-Security', 'max-age='.(60 * 60 * 24 * 126)); // 126 days
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
}
$response
@@ -197,11 +220,13 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$origin = $request->getOrigin($request->getReferer(''));
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
if (!$originValidator->isValid($origin)
if (
!$originValidator->isValid($origin)
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))) {
throw new Exception($originValidator->getDescription(), 403, Exception::GENERAL_UNKNOWN_ORIGIN);
&& empty($request->getHeader('x-appwrite-key', ''))
) {
throw new AppwriteException($originValidator->getDescription(), 403, AppwriteException::GENERAL_UNKNOWN_ORIGIN);
}
/*
@@ -246,7 +271,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$user = new Document([
'$id' => '',
'status' => true,
'email' => 'app.'.$project->getId().'@service.'.$request->getHostname(),
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
]);
@@ -254,47 +279,47 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$role = Auth::USER_ROLE_APP;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
Authorization::setRole('role:'.Auth::USER_ROLE_APP);
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
}
}
Authorization::setRole('role:'.$role);
Authorization::setRole('role:' . $role);
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
}
$service = $route->getLabel('sdk.namespace','');
if(!empty($service)) {
if(array_key_exists($service, $project->getAttribute('services',[]))
&& !$project->getAttribute('services',[])[$service]
&& !Auth::isPrivilegedUser(Authorization::getRoles())) {
throw new Exception('Service is disabled', 503, Exception::GENERAL_SERVICE_DISABLED);
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
$roles = Authorization::getRoles();
if (
array_key_exists($service, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser($roles) || Auth::isAppUser($roles))
) {
throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED);
}
}
if (!\in_array($scope, $scopes)) {
if ($project->isEmpty()) { // Check if permission is denied because project is missing
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
}
throw new Exception($user->getAttribute('email', 'User').' (role: '.\strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401, Exception::GENERAL_UNAUTHORIZED_SCOPE);
throw new AppwriteException($user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')', 401, AppwriteException::GENERAL_UNAUTHORIZED_SCOPE);
}
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception('Invalid credentials. User is blocked', 401, Exception::USER_BLOCKED);
throw new AppwriteException('Invalid credentials. User is blocked', 401, AppwriteException::USER_BLOCKED);
}
if ($user->getAttribute('reset')) {
throw new Exception('Password reset is required', 412, Exception::USER_PASSWORD_RESET_REQUIRED);
throw new AppwriteException('Password reset is required', 412, AppwriteException::USER_PASSWORD_RESET_REQUIRED);
}
}, ['utopia', 'request', 'response', 'console', 'project', 'dbForConsole', 'user', 'locale', 'clients']);
App::options(function ($request, $response) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
App::options(function (Request $request, Response $response) {
$origin = $request->getOrigin();
@@ -308,15 +333,7 @@ App::options(function ($request, $response) {
->noContent();
}, ['request', 'response']);
App::error(function ($error, $utopia, $request, $response, $layout, $project, $logger, $loggerBreadcrumbs) {
/** @var Exception $error */
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Logger\Logger $logger */
/** @var Utopia\Logger\Log\Breadcrumb[] $loggerBreadcrumbs */
App::error(function (Throwable $error, App $utopia, Request $request, Response $response, View $layout, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->match($request);
@@ -326,18 +343,18 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
throw $error;
}
if($logger) {
if($error->getCode() >= 500 || $error->getCode() === 0) {
if ($logger) {
if ($error->getCode() >= 500 || $error->getCode() === 0) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
} catch(\Throwable $th) {
} catch (\Throwable $th) {
// All good, user is optional information for logger
}
$log = new Utopia\Logger\Log();
if(isset($user) && !$user->isEmpty()) {
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
}
@@ -348,7 +365,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
$log->setMessage($error->getMessage());
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('url', $route->getPath());
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId());
@@ -367,12 +384,12 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
foreach($loggerBreadcrumbs as $loggerBreadcrumb) {
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
$log->addBreadcrumb($loggerBreadcrumb);
}
$responseCode = $logger->addLog($log);
Console::info('Log pushed with status code: '.$responseCode);
Console::info('Log pushed with status code: ' . $responseCode);
}
}
@@ -383,35 +400,35 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
Console::error('[Error] Timestamp: '.date('c', time()));
Console::error('[Error] Timestamp: ' . date('c', time()));
if($route) {
Console::error('[Error] Method: '.$route->getMethod());
Console::error('[Error] URL: '.$route->getPath());
if ($route) {
Console::error('[Error] Method: ' . $route->getMethod());
Console::error('[Error] URL: ' . $route->getPath());
}
Console::error('[Error] Type: '.get_class($error));
Console::error('[Error] Message: '.$message);
Console::error('[Error] File: '.$file);
Console::error('[Error] Line: '.$line);
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $message);
Console::error('[Error] File: ' . $file);
Console::error('[Error] Line: ' . $line);
}
/** Handle Utopia Errors */
if ($error instanceof Utopia\Exception) {
$error = new Exception($message, $code, Exception::GENERAL_UNKNOWN, $error);
switch($code) {
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
switch ($code) {
case 400:
$error->setType(Exception::GENERAL_ARGUMENT_INVALID);
$error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID);
break;
case 404:
$error->setType(Exception::GENERAL_ROUTE_NOT_FOUND);
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
break;
}
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if (!($error instanceof Exception)) {
$error = new Exception($message, $code, Exception::GENERAL_UNKNOWN, $error);
if (!($error instanceof AppwriteException)) {
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
}
switch ($code) { // Don't show 500 errors!
@@ -473,7 +490,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
;
$layout
->setParam('title', $project->getAttribute('name').' - Error')
->setParam('title', $project->getAttribute('name') . ' - Error')
->setParam('description', 'No Description')
->setParam('body', $comp)
->setParam('version', $version)
@@ -483,8 +500,10 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
$response->html($layout->render());
}
$response->dynamic(new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR);
$response->dynamic(
new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
);
}, ['error', 'utopia', 'request', 'response', 'layout', 'project', 'logger', 'loggerBreadcrumbs']);
App::get('/manifest.json')
@@ -492,8 +511,7 @@ App::get('/manifest.json')
->label('scope', 'public')
->label('docs', false)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->json([
'name' => APP_NAME,
@@ -519,8 +537,8 @@ App::get('/robots.txt')
->label('scope', 'public')
->label('docs', false)
->inject('response')
->action(function ($response) {
$template = new View(__DIR__.'/../views/general/robots.phtml');
->action(function (Response $response) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
});
@@ -529,8 +547,8 @@ App::get('/humans.txt')
->label('scope', 'public')
->label('docs', false)
->inject('response')
->action(function ($response) {
$template = new View(__DIR__.'/../views/general/humans.phtml');
->action(function (Response $response) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
});
@@ -540,7 +558,7 @@ App::get('/.well-known/acme-challenge')
->label('docs', false)
->inject('request')
->inject('response')
->action(function ($request, $response) {
->action(function (Request $request, Response $response) {
$uriChunks = \explode('/', $request->getURI());
$token = $uriChunks[\count($uriChunks) - 1];
@@ -553,32 +571,32 @@ App::get('/.well-known/acme-challenge')
]);
if (!$validator->isValid($token) || \count($uriChunks) !== 4) {
throw new Exception('Invalid challenge token.', 400);
throw new AppwriteException('Invalid challenge token.', 400);
}
$base = \realpath(APP_STORAGE_CERTIFICATES);
$absolute = \realpath($base.'/.well-known/acme-challenge/'.$token);
$absolute = \realpath($base . '/.well-known/acme-challenge/' . $token);
if (!$base) {
throw new Exception('Storage error', 500, Exception::GENERAL_SERVER_ERROR);
throw new AppwriteException('Storage error', 500, AppwriteException::GENERAL_SERVER_ERROR);
}
if (!$absolute) {
throw new Exception('Unknown path', 404);
throw new AppwriteException('Unknown path', 404);
}
if (!\substr($absolute, 0, \strlen($base)) === $base) {
throw new Exception('Invalid path', 401);
throw new AppwriteException('Invalid path', 401);
}
if (!\file_exists($absolute)) {
throw new Exception('Unknown path', 404);
throw new AppwriteException('Unknown path', 404);
}
$content = @\file_get_contents($absolute);
if (!$content) {
throw new Exception('Failed to get contents', 500, Exception::GENERAL_SERVER_ERROR);
throw new AppwriteException('Failed to get contents', 500, AppwriteException::GENERAL_SERVER_ERROR);
}
$response->text($content);
+48 -64
View File
@@ -5,6 +5,7 @@ global $utopia, $request, $response;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Appwrite\Network\Validator\Host;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Validator\ArrayList;
@@ -27,7 +28,7 @@ App::get('/v1/mock/tests/foo')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($x, $y, $z) {
});
@@ -45,7 +46,7 @@ App::post('/v1/mock/tests/foo')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($x, $y, $z) {
});
@@ -63,7 +64,7 @@ App::patch('/v1/mock/tests/foo')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($x, $y, $z) {
});
@@ -81,7 +82,7 @@ App::put('/v1/mock/tests/foo')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($x, $y, $z) {
});
@@ -99,7 +100,7 @@ App::delete('/v1/mock/tests/foo')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($x, $y, $z) {
});
@@ -117,7 +118,7 @@ App::get('/v1/mock/tests/bar')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($required, $default, $z) {
});
@@ -135,7 +136,7 @@ App::post('/v1/mock/tests/bar')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($required, $default, $z) {
});
@@ -153,7 +154,7 @@ App::patch('/v1/mock/tests/bar')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($required, $default, $z) {
});
@@ -171,7 +172,7 @@ App::put('/v1/mock/tests/bar')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($required, $default, $z) {
});
@@ -189,7 +190,7 @@ App::delete('/v1/mock/tests/bar')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->action(function ($required, $default, $z) {
});
@@ -206,13 +207,12 @@ App::get('/v1/mock/tests/general/download')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.mock', true)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Request $request */
->action(function (Response $response) {
$response
->setContentType('text/plain')
->addHeader('Content-Disposition', 'attachment; filename="test.txt"')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->addHeader('X-Peak', \memory_get_peak_usage())
->send("Download test passed.")
;
@@ -233,58 +233,56 @@ App::post('/v1/mock/tests/general/upload')
->label('sdk.mock', true)
->param('x', '', new Text(100), 'Sample string param')
->param('y', '', new Integer(true), 'Sample numeric param')
->param('z', null, new ArrayList(new Text(256)), 'Sample array param')
->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param')
->param('file', [], new File(), 'Sample file param', false)
->inject('request')
->inject('response')
->action(function ($x, $y, $z, $file, $request, $response) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Swoole\Response $response */
->action(function (string $x, int $y, array $z, array $file, Request $request, Response $response) {
$file = $request->getFiles('file');
$contentRange = $request->getHeader('content-range');
$chunkSize = 5*1024*1024; // 5MB
$chunkSize = 5 * 1024 * 1024; // 5MB
if(!empty($contentRange)) {
if (!empty($contentRange)) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$size = $request->getContentRangeSize();
$id = $request->getHeader('x-appwrite-id', '');
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
if(is_null($start) || is_null($end) || is_null($size)) {
if (is_null($start) || is_null($end) || is_null($size)) {
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
}
if($start > $end || $end > $size) {
if ($start > $end || $end > $size) {
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
}
if($start === 0 && !empty($id)) {
if ($start === 0 && !empty($id)) {
throw new Exception('First chunked request cannot have id header', 400, Exception::GENERAL_MOCK);
}
if($start !== 0 && $id !== 'newfileid') {
if ($start !== 0 && $id !== 'newfileid') {
throw new Exception('All chunked request must have id header (except first)', 400, Exception::GENERAL_MOCK);
}
if($end !== $size && $end-$start+1 !== $chunkSize) {
if ($end !== $size && $end - $start + 1 !== $chunkSize) {
throw new Exception('Chunk size must be 5MB (except last chunk)', 400, Exception::GENERAL_MOCK);
}
if ($end !== $size && $file['size'] !== $chunkSize) {
throw new Exception('Wrong chunk size', 400, Exception::GENERAL_MOCK);
}
if($file['size'] > $chunkSize) {
if ($file['size'] > $chunkSize) {
throw new Exception('Chunk size must be 5MB or less', 400, Exception::GENERAL_MOCK);
}
if($end !== $size) {
if ($end !== $size) {
$response->json([
'$id'=> 'newfileid',
'$id' => 'newfileid',
'chunksTotal' => $file['size'] / $chunkSize,
'chunksUploaded' => $start / $chunkSize
]);
@@ -293,11 +291,11 @@ App::post('/v1/mock/tests/general/upload')
$file['tmp_name'] = (\is_array($file['tmp_name'])) ? $file['tmp_name'][0] : $file['tmp_name'];
$file['name'] = (\is_array($file['name'])) ? $file['name'][0] : $file['name'];
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
if ($file['name'] !== 'file.png') {
throw new Exception('Wrong file name', 400, Exception::GENERAL_MOCK);
}
if ($file['size'] !== 38756) {
throw new Exception('Wrong file size', 400, Exception::GENERAL_MOCK);
}
@@ -321,8 +319,7 @@ App::get('/v1/mock/tests/general/redirect')
->label('sdk.response.model', Response::MODEL_MOCK)
->label('sdk.mock', true)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->redirect('/v1/mock/tests/general/redirect/done');
});
@@ -356,9 +353,7 @@ App::get('/v1/mock/tests/general/set-cookie')
->label('sdk.mock', true)
->inject('response')
->inject('request')
->action(function ($response, $request) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\Request $request */
->action(function (Response $response, Request $request) {
$response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', $request->getHostname(), true, true);
});
@@ -376,8 +371,7 @@ App::get('/v1/mock/tests/general/get-cookie')
->label('sdk.response.model', Response::MODEL_MOCK)
->label('sdk.mock', true)
->inject('request')
->action(function ($request) {
/** @var Appwrite\Utopia\Request $request */
->action(function (Request $request) {
if ($request->getCookie('cookieName', '') !== 'cookieValue') {
throw new Exception('Missing cookie value', 400, Exception::GENERAL_MOCK);
@@ -396,8 +390,7 @@ App::get('/v1/mock/tests/general/empty')
->label('sdk.response.model', Response::MODEL_NONE)
->label('sdk.mock', true)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->dynamic(new Document(), Response::MODEL_NONE);
});
@@ -447,8 +440,7 @@ App::get('/v1/mock/tests/general/502-error')
->label('sdk.response.model', Response::MODEL_ANY)
->label('sdk.mock', true)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response
->setStatusCode(502)
@@ -467,10 +459,9 @@ App::get('/v1/mock/tests/general/oauth2')
->param('scope', '', new Text(100), 'OAuth2 scope list.')
->param('state', '', new Text(1024), 'OAuth2 state.')
->inject('response')
->action(function ($client_id, $redirectURI, $scope, $state, $response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (string $client_id, string $redirectURI, string $scope, string $state, Response $response) {
$response->redirect($redirectURI.'?'.\http_build_query(['code' => 'abcdef', 'state' => $state]));
$response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state]));
});
App::get('/v1/mock/tests/general/oauth2/token')
@@ -486,8 +477,7 @@ App::get('/v1/mock/tests/general/oauth2/token')
->param('code', '', new Text(100), 'OAuth2 state.', true)
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
->inject('response')
->action(function ($client_id, $client_secret, $grantType, $redirectURI, $code, $refreshToken, $response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (string $client_id, string $client_secret, string $grantType, string $redirectURI, string $code, string $refreshToken, Response $response) {
if ($client_id != '1') {
throw new Exception('Invalid client ID', 400, Exception::GENERAL_MOCK);
@@ -503,13 +493,13 @@ App::get('/v1/mock/tests/general/oauth2/token')
'expires_in' => 14400
];
if($grantType === 'authorization_code') {
if ($grantType === 'authorization_code') {
if ($code !== 'abcdef') {
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
}
$response->json($responseJson);
} else if($grantType === 'refresh_token') {
} elseif ($grantType === 'refresh_token') {
if ($refreshToken !== 'tuvwxyz') {
throw new Exception('Invalid refresh token', 400, Exception::GENERAL_MOCK);
}
@@ -527,8 +517,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
->label('docs', false)
->param('token', '', new Text(100), 'OAuth2 Access Token.')
->inject('response')
->action(function ($token, $response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (string $token, Response $response) {
if ($token != '123456') {
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
@@ -547,8 +536,7 @@ App::get('/v1/mock/tests/general/oauth2/success')
->label('scope', 'public')
->label('docs', false)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response->json([
'result' => 'success',
@@ -561,8 +549,7 @@ App::get('/v1/mock/tests/general/oauth2/failure')
->label('scope', 'public')
->label('docs', false)
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$response
->setStatusCode(Response::STATUS_CODE_BAD_REQUEST)
@@ -571,16 +558,13 @@ App::get('/v1/mock/tests/general/oauth2/failure')
]);
});
App::shutdown(function($utopia, $response, $request) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
App::shutdown(function (App $utopia, Response $response, Request $request) {
$result = [];
$route = $utopia->match($request);
$path = APP_STORAGE_CACHE.'/tests.json';
$path = APP_STORAGE_CACHE . '/tests.json';
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
if (!\is_array($tests)) {
throw new Exception('Failed to read results', 500, Exception::GENERAL_MOCK);
}
@@ -594,4 +578,4 @@ App::shutdown(function($utopia, $response, $request) {
}
$response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK);
}, ['utopia', 'response', 'request'], 'mock');
}, ['utopia', 'response', 'request'], 'mock');
+91 -110
View File
@@ -1,32 +1,24 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Event\Audit;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Request;
use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Database\Validator\Authorization;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Storage;
use Utopia\Registry\Registry;
App::init(function ($utopia, $request, $response, $project, $user, $events, $audits, $usage, $deletes, $database, $dbForProject, $mode) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Registry\Registry $register */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $functions */
/** @var Utopia\Database\Database $dbForProject */
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) {
$route = $utopia->match($request);
@@ -48,7 +40,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname().$route->getPath());
->setParam('{url}', $request->getHostname() . $route->getPath());
$timeLimitArray[] = $timeLimit;
}
@@ -60,8 +52,8 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if(!empty($value)) {
$timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value);
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
@@ -76,10 +68,11 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
;
}
if ((App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited
&& $abuse->check()) // Abuse is not disabled
&& (!$isAppUser && !$isPrivilegedUser)) // User is not an admin or API key
{
if (
(App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited
&& $abuse->check()) // Abuse is not disabled
&& (!$isAppUser && !$isPrivilegedUser)
) { // User is not an admin or API key
throw new Exception('Too many requests', 429, Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
@@ -88,91 +81,79 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
* Background Jobs
*/
$events
->setParam('projectId', $project->getId())
->setParam('webhooks', $project->getAttribute('webhooks', []))
->setParam('userId', $user->getId())
->setParam('event', $route->getLabel('event', ''))
->setParam('eventData', [])
->setParam('functionId', null)
->setParam('executionId', null)
->setParam('trigger', 'event')
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
$mails
->setProject($project)
->setUser($user)
;
$audits
->setParam('projectId', $project->getId())
->setParam('userId', $user->getId())
->setParam('userEmail', $user->getAttribute('email'))
->setParam('userName', $user->getAttribute('name'))
->setParam('mode', $mode)
->setParam('event', '')
->setParam('resource', '')
->setParam('userAgent', $request->getUserAgent(''))
->setParam('ip', $request->getIP())
->setParam('data', [])
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
$usage
->setParam('projectId', $project->getId())
->setParam('httpRequest', 1)
->setParam('httpUrl', $request->getHostname().$request->getURI())
->setParam('httpUrl', $request->getHostname() . $request->getURI())
->setParam('httpMethod', $request->getMethod())
->setParam('httpPath', $route->getPath())
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->setParam('storage', 0)
;
$deletes
->setParam('projectId', $project->getId())
;
$database
->setParam('projectId', $project->getId())
;
}, ['utopia', 'request', 'response', 'project', 'user', 'events', 'audits', 'usage', 'deletes', 'database', 'dbForProject', 'mode'], 'api');
$deletes->setProject($project);
$database->setProject($project);
}, ['utopia', 'request', 'response', 'project', 'user', 'events', 'audits', 'mails', 'usage', 'deletes', 'database', 'dbForProject', 'mode'], 'api');
App::init(function ($utopia, $request, $project) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Document $project */
App::init(function (App $utopia, Request $request, Document $project) {
$route = $utopia->match($request);
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
if($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
}
$auths = $project->getAttribute('auths', []);
switch ($route->getLabel('auth.type', '')) {
case 'emailPassword':
if(($auths['emailPassword'] ?? true) === false) {
if (($auths['emailPassword'] ?? true) === false) {
throw new Exception('Email / Password authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
}
break;
case 'magic-url':
if($project->getAttribute('usersAuthMagicURL', true) === false) {
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
throw new Exception('Magic URL authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
}
break;
case 'anonymous':
if(($auths['anonymous'] ?? true) === false) {
if (($auths['anonymous'] ?? true) === false) {
throw new Exception('Anonymous authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
}
break;
case 'invites':
if(($auths['invites'] ?? true) === false) {
if (($auths['invites'] ?? true) === false) {
throw new Exception('Invites authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
}
break;
case 'jwt':
if(($auths['JWT'] ?? true) === false) {
if (($auths['JWT'] ?? true) === false) {
throw new Exception('JWT authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
}
break;
@@ -181,89 +162,89 @@ App::init(function ($utopia, $request, $project) {
throw new Exception('Unsupported authentication route', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
break;
}
}, ['utopia', 'request', 'project'], 'auth');
App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $database, $mode) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $deletes */
/** @var Appwrite\Event\Event $database */
/** @var bool $mode */
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) {
if (!empty($events->getParam('event'))) {
if (empty($events->getParam('eventData'))) {
$events->setParam('eventData', $response->getPayload());
if (!empty($events->getEvent())) {
if (empty($events->getPayload())) {
$events->setPayload($response->getPayload());
}
$webhooks = clone $events;
$functions = clone $events;
$webhooks
->setQueue('v1-webhooks')
->setClass('WebhooksV1')
/**
* Trigger functions.
*/
$events
->setClass(Event::FUNCTIONS_CLASS_NAME)
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
->trigger();
$functions
->setQueue('v1-functions')
->setClass('FunctionsV1')
/**
* Trigger webhooks.
*/
$events
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->trigger();
/**
* Trigger realtime.
*/
if ($project->getId() !== 'console') {
$payload = new Document($response->getPayload());
$collection = new Document($events->getParam('collection') ?? []);
$bucket = new Document($events->getParam('bucket') ?? []);
$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;
$target = Realtime::fromPayload(
event: $events->getParam('event'),
payload: $payload,
project: $project,
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $project,
collection: $collection,
bucket: $bucket,
);
Realtime::send(
$target['projectId'] ?? $project->getId(),
$response->getPayload(),
$events->getParam('event'),
$target['channels'],
$target['roles'],
[
'permissionsChanged' => $target['permissionsChanged'],
projectId: $target['projectId'] ?? $project->getId(),
payload: $events->getPayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $events->getParam('userId')
]
);
}
}
if (!empty($audits->getParam('event'))) {
if (!empty($audits->getResource())) {
foreach ($events->getParams() as $key => $value) {
$audits->setParam($key, $value);
}
$audits->trigger();
}
if (!empty($deletes->getParam('type')) && !empty($deletes->getParam('document'))) {
if (!empty($deletes->getType())) {
$deletes->trigger();
}
if (!empty($database->getParam('type')) && !empty($database->getParam('document'))) {
if (!empty($database->getType())) {
$database->trigger();
}
$route = $utopia->match($request);
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
if (
App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
&& $project->getId()
&& $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin
&& !empty($route->getLabel('sdk.namespace', null))) { // Don't calculate console usage on admin mode
&& !empty($route->getLabel('sdk.namespace', null))
) { // Don't calculate console usage on admin mode
$usage
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize())
->submit()
;
->submit();
}
}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'database', 'mode'], 'api');
}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'database', 'mode', 'dbForProject'], 'api');
+6 -7
View File
@@ -2,12 +2,11 @@
use Utopia\App;
use Utopia\Config\Config;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\View;
App::init(function ($utopia, $request, $response, $layout) {
/** @var Utopia\App $utopia */
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
App::init(function (App $utopia, Request $request, Response $response, View $layout) {
/* AJAX check */
if (!empty($request->getQuery('version', ''))) {
@@ -48,10 +47,10 @@ App::init(function ($utopia, $request, $response, $layout) {
$route = $utopia->match($request);
$route->label('error', __DIR__.'/../../views/general/error.phtml');
$route->label('error', __DIR__ . '/../../views/general/error.phtml');
$scope = $route->getLabel('scope', '');
$layout
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
->setParam('isDev', App::isDevelopment())
+114 -98
View File
@@ -1,6 +1,7 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Config\Config;
@@ -8,8 +9,7 @@ use Utopia\Domains\Domain;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
App::init(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
App::init(function (View $layout) {
$layout
->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.')
@@ -17,12 +17,10 @@ App::init(function ($layout) {
;
}, ['layout'], 'console');
App::shutdown(function ($response, $layout) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
App::shutdown(function (Response $response, View $layout) {
$header = new View(__DIR__.'/../../views/console/comps/header.phtml');
$footer = new View(__DIR__.'/../../views/console/comps/footer.phtml');
$header = new View(__DIR__ . '/../../views/console/comps/header.phtml');
$footer = new View(__DIR__ . '/../../views/console/comps/footer.phtml');
$footer
->setParam('home', App::getEnv('_APP_HOME', ''))
@@ -43,17 +41,16 @@ App::get('/error/:code')
->label('scope', 'home')
->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false)
->inject('layout')
->action(function ($code, $layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (int $code, View $layout) {
$page = new View(__DIR__.'/../../views/error.phtml');
$page = new View(__DIR__ . '/../../views/error.phtml');
$page
->setParam('code', $code)
;
$layout
->setParam('title', APP_NAME.' - Error')
->setParam('title', APP_NAME . ' - Error')
->setParam('body', $page);
});
@@ -62,17 +59,16 @@ App::get('/console')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/index.phtml');
$page = new View(__DIR__ . '/../../views/console/index.phtml');
$page
->setParam('home', App::getEnv('_APP_HOME', ''))
;
$layout
->setParam('title', APP_NAME.' - Console')
->setParam('title', APP_NAME . ' - Console')
->setParam('body', $page);
});
@@ -81,19 +77,18 @@ App::get('/console/account')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/account/index.phtml');
$page = new View(__DIR__ . '/../../views/console/account/index.phtml');
$cc = new View(__DIR__.'/../../views/console/forms/credit-card.phtml');
$cc = new View(__DIR__ . '/../../views/console/forms/credit-card.phtml');
$page
->setParam('cc', $cc)
;
$layout
->setParam('title', 'Account - '.APP_NAME)
->setParam('title', 'Account - ' . APP_NAME)
->setParam('body', $page);
});
@@ -102,13 +97,12 @@ App::get('/console/notifications')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/v1/console/notifications/index.phtml');
$page = new View(__DIR__ . '/../../views/v1/console/notifications/index.phtml');
$layout
->setParam('title', APP_NAME.' - Notifications')
->setParam('title', APP_NAME . ' - Notifications')
->setParam('body', $page);
});
@@ -117,14 +111,13 @@ App::get('/console/home')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/home/index.phtml');
$page = new View(__DIR__ . '/../../views/console/home/index.phtml');
$page
->setParam('usageStatsEnabled',App::getEnv('_APP_USAGE_STATS','enabled') == 'enabled');
->setParam('usageStatsEnabled', App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled');
$layout
->setParam('title', APP_NAME.' - Console')
->setParam('title', APP_NAME . ' - Console')
->setParam('body', $page);
});
@@ -133,22 +126,20 @@ App::get('/console/settings')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
$page = new View(__DIR__.'/../../views/console/settings/index.phtml');
$page = new View(__DIR__ . '/../../views/console/settings/index.phtml');
$page
->setParam('services', array_filter(Config::getParam('services'), function($element) {return $element['optional'];}))
$page->setParam('services', array_filter(Config::getParam('services'), fn($element) => $element['optional']))
->setParam('customDomainsEnabled', ($target->isKnown() && !$target->isTest()))
->setParam('customDomainsTarget', $target->get())
->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST'))))
;
$layout
->setParam('title', APP_NAME.' - Settings')
->setParam('title', APP_NAME . ' - Settings')
->setParam('body', $page);
});
@@ -157,17 +148,53 @@ App::get('/console/webhooks')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/webhooks/index.phtml');
$page = new View(__DIR__ . '/../../views/console/webhooks/index.phtml');
$page->setParam('events', Config::getParam('events', []));
$layout
->setParam('title', APP_NAME . ' - Webhooks')
->setParam('body', $page);
});
App::get('/console/webhooks/webhook')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->param('id', '', new UID(), 'Webhook unique ID.')
->inject('layout')
->action(function (string $id, View $layout) {
$page = new View(__DIR__ . '/../../views/console/webhooks/webhook.phtml');
$page
->setParam('events', Config::getParam('events', []))
->setParam('new', false)
;
$layout
->setParam('title', APP_NAME.' - Webhooks')
->setParam('title', APP_NAME . ' - Webhooks')
->setParam('body', $page);
});
App::get('/console/webhooks/webhook/new')
->groups(['web', 'console'])
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/console/webhooks/webhook.phtml');
$page
->setParam('events', Config::getParam('events', []))
->setParam('new', true)
;
$layout
->setParam('title', APP_NAME . ' - Webhooks')
->setParam('body', $page);
});
@@ -176,16 +203,15 @@ App::get('/console/keys')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$scopes = array_keys(Config::getParam('scopes'));
$page = new View(__DIR__.'/../../views/console/keys/index.phtml');
$page = new View(__DIR__ . '/../../views/console/keys/index.phtml');
$page->setParam('scopes', $scopes);
$layout
->setParam('title', APP_NAME.' - API Keys')
->setParam('title', APP_NAME . ' - API Keys')
->setParam('body', $page);
});
@@ -194,13 +220,12 @@ App::get('/console/database')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/database/index.phtml');
$page = new View(__DIR__ . '/../../views/console/database/index.phtml');
$layout
->setParam('title', APP_NAME.' - Database')
->setParam('title', APP_NAME . ' - Database')
->setParam('body', $page);
});
@@ -211,11 +236,9 @@ App::get('/console/database/collection')
->param('id', '', new UID(), 'Collection unique ID.')
->inject('response')
->inject('layout')
->action(function ($id, $response, $layout) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
->action(function (string $id, Response $response, View $layout) {
$logs = new View(__DIR__.'/../../views/console/comps/logs.phtml');
$logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
@@ -225,12 +248,12 @@ App::get('/console/database/collection')
])
;
$page = new View(__DIR__.'/../../views/console/database/collection.phtml');
$page = new View(__DIR__ . '/../../views/console/database/collection.phtml');
$page->setParam('logs', $logs);
$layout
->setParam('title', APP_NAME.' - Database Collection')
->setParam('title', APP_NAME . ' - Database Collection')
->setParam('body', $page)
;
@@ -247,10 +270,9 @@ App::get('/console/database/document')
->label('scope', 'console')
->param('collection', '', new UID(), 'Collection unique ID.')
->inject('layout')
->action(function ($collection, $layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (string $collection, View $layout) {
$logs = new View(__DIR__.'/../../views/console/comps/logs.phtml');
$logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
@@ -261,7 +283,7 @@ App::get('/console/database/document')
])
;
$page = new View(__DIR__.'/../../views/console/database/document.phtml');
$page = new View(__DIR__ . '/../../views/console/database/document.phtml');
$page
->setParam('new', false)
@@ -270,7 +292,7 @@ App::get('/console/database/document')
;
$layout
->setParam('title', APP_NAME.' - Database Document')
->setParam('title', APP_NAME . ' - Database Document')
->setParam('body', $page);
});
@@ -280,10 +302,9 @@ App::get('/console/database/document/new')
->label('scope', 'console')
->param('collection', '', new UID(), 'Collection unique ID.')
->inject('layout')
->action(function ($collection, $layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (string $collection, View $layout) {
$page = new View(__DIR__.'/../../views/console/database/document.phtml');
$page = new View(__DIR__ . '/../../views/console/database/document.phtml');
$page
->setParam('new', true)
@@ -292,7 +313,7 @@ App::get('/console/database/document/new')
;
$layout
->setParam('title', APP_NAME.' - Database Document')
->setParam('title', APP_NAME . ' - Database Document')
->setParam('body', $page);
});
@@ -301,10 +322,10 @@ App::get('/console/storage')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
$page = new View(__DIR__.'/../../views/console/storage/index.phtml');
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/console/storage/index.phtml');
$page
->setParam('home', App::getEnv('_APP_HOME', 0))
->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0))
@@ -312,7 +333,7 @@ App::get('/console/storage')
;
$layout
->setParam('title', APP_NAME.' - Storage')
->setParam('title', APP_NAME . ' - Storage')
->setParam('body', $page);
});
@@ -323,19 +344,17 @@ App::get('/console/storage/bucket')
->param('id', '', new UID(), 'Bucket unique ID.')
->inject('response')
->inject('layout')
->action(function ($id, $response, $layout) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\View $layout */
->action(function (string $id, Response $response, View $layout) {
$page = new View(__DIR__.'/../../views/console/storage/bucket.phtml');
$page = new View(__DIR__ . '/../../views/console/storage/bucket.phtml');
$page
->setParam('home', App::getEnv('_APP_HOME', 0))
->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0))
->setParam('fileLimitHuman', Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0)))
;
$layout
->setParam('title', APP_NAME.' - Storage Buckets')
->setParam('title', APP_NAME . ' - Storage Buckets')
->setParam('body', $page)
;
@@ -351,10 +370,9 @@ App::get('/console/users')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/users/index.phtml');
$page = new View(__DIR__ . '/../../views/console/users/index.phtml');
$page
->setParam('auth', Config::getParam('auth'))
@@ -363,7 +381,7 @@ App::get('/console/users')
;
$layout
->setParam('title', APP_NAME.' - Users')
->setParam('title', APP_NAME . ' - Users')
->setParam('body', $page);
});
@@ -372,13 +390,12 @@ App::get('/console/users/user')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/users/user.phtml');
$page = new View(__DIR__ . '/../../views/console/users/user.phtml');
$layout
->setParam('title', APP_NAME.' - User')
->setParam('title', APP_NAME . ' - User')
->setParam('body', $page);
});
@@ -387,13 +404,12 @@ App::get('/console/users/teams/team')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/console/users/team.phtml');
$page = new View(__DIR__ . '/../../views/console/users/team.phtml');
$layout
->setParam('title', APP_NAME.' - Team')
->setParam('title', APP_NAME . ' - Team')
->setParam('body', $page);
});
@@ -403,15 +419,15 @@ App::get('/console/functions')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
$page = new View(__DIR__.'/../../views/console/functions/index.phtml');
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/console/functions/index.phtml');
$page
->setParam('runtimes', Config::getParam('runtimes'))
;
$layout
->setParam('title', APP_NAME.' - Functions')
->setParam('title', APP_NAME . ' - Functions')
->setParam('body', $page);
});
@@ -421,19 +437,19 @@ App::get('/console/functions/function')
->label('permission', 'public')
->label('scope', 'console')
->inject('layout')
->action(function ($layout) {
$page = new View(__DIR__.'/../../views/console/functions/function.phtml');
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/console/functions/function.phtml');
$page
->setParam('events', Config::getParam('events', []))
->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0))
->setParam('fileLimitHuman', Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0)))
->setParam('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))
->setParam('usageStatsEnabled',App::getEnv('_APP_USAGE_STATS','enabled') == 'enabled');
->setParam('usageStatsEnabled', App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled');
;
$layout
->setParam('title', APP_NAME.' - Function')
->setParam('title', APP_NAME . ' - Function')
->setParam('body', $page);
});
@@ -445,8 +461,8 @@ App::get('/console/version')
->inject('response')
->action(function ($response) {
try {
$version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost').'/v1/health/version'), true);
$version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost') . '/v1/health/version'), true);
if ($version && isset($version['version'])) {
return $response->json(['version' => $version['version']]);
} else {
@@ -455,4 +471,4 @@ App::get('/console/version')
} catch (\Throwable $th) {
throw new Exception('Failed to check for a newer version', 500, Exception::GENERAL_SERVER_ERROR);
}
});
});
+42 -55
View File
@@ -1,14 +1,16 @@
<?php
use Appwrite\Utopia\View;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
App::init(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
App::init(function (View $layout) {
$header = new View(__DIR__.'/../../views/home/comps/header.phtml');
$footer = new View(__DIR__.'/../../views/home/comps/footer.phtml');
$header = new View(__DIR__ . '/../../views/home/comps/header.phtml');
$footer = new View(__DIR__ . '/../../views/home/comps/footer.phtml');
$footer
->setParam('version', App::getEnv('_APP_VERSION', 'UNKNOWN'))
@@ -24,9 +26,7 @@ App::init(function ($layout) {
;
}, ['layout'], 'home');
App::shutdown(function ($response, $layout) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Utopia\View $layout */
App::shutdown(function (Response $response, View $layout) {
$response->html($layout->render());
}, ['response', 'layout'], 'home');
@@ -38,10 +38,7 @@ App::get('/')
->inject('response')
->inject('dbForConsole')
->inject('project')
->action(function ($response, $dbForConsole, $project) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Document $project */
->action(function (Response $response, Database $dbForConsole, Document $project) {
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
@@ -52,10 +49,10 @@ App::get('/')
if ('console' === $project->getId() || $project->isEmpty()) {
$whitelistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled');
if($whitelistRoot !== 'disabled') {
if ($whitelistRoot !== 'disabled') {
$count = $dbForConsole->count('users', [], 1);
if($count !== 0) {
if ($count !== 0) {
return $response->redirect('/auth/signin');
}
}
@@ -69,17 +66,16 @@ App::get('/auth/signin')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/signin.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/signin.phtml');
$page
->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled'))
;
$layout
->setParam('title', 'Sign In - '.APP_NAME)
->setParam('title', 'Sign In - ' . APP_NAME)
->setParam('body', $page);
});
@@ -88,16 +84,16 @@ App::get('/auth/signup')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
$page = new View(__DIR__.'/../../views/home/auth/signup.phtml');
->action(function (View $layout) {
$page = new View(__DIR__ . '/../../views/home/auth/signup.phtml');
$page
->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled'))
;
$layout
->setParam('title', 'Sign Up - '.APP_NAME)
->setParam('title', 'Sign Up - ' . APP_NAME)
->setParam('body', $page);
});
@@ -106,17 +102,16 @@ App::get('/auth/recovery')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/recovery.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/recovery.phtml');
$page
->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST'))))
;
$layout
->setParam('title', 'Password Recovery - '.APP_NAME)
->setParam('title', 'Password Recovery - ' . APP_NAME)
->setParam('body', $page);
});
@@ -125,13 +120,12 @@ App::get('/auth/confirm')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/confirm.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/confirm.phtml');
$layout
->setParam('title', 'Account Confirmation - '.APP_NAME)
->setParam('title', 'Account Confirmation - ' . APP_NAME)
->setParam('body', $page);
});
@@ -140,13 +134,12 @@ App::get('/auth/join')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/join.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/join.phtml');
$layout
->setParam('title', 'Invitation - '.APP_NAME)
->setParam('title', 'Invitation - ' . APP_NAME)
->setParam('body', $page);
});
@@ -155,13 +148,12 @@ App::get('/auth/recovery/reset')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/recovery/reset.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/recovery/reset.phtml');
$layout
->setParam('title', 'Password Reset - '.APP_NAME)
->setParam('title', 'Password Reset - ' . APP_NAME)
->setParam('body', $page);
});
@@ -170,10 +162,9 @@ App::get('/auth/oauth2/success')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/oauth2.phtml');
$layout
->setParam('title', APP_NAME)
@@ -188,10 +179,9 @@ App::get('/auth/magic-url')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/magicURL.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/magicURL.phtml');
$layout
->setParam('title', APP_NAME)
@@ -206,10 +196,9 @@ App::get('/auth/oauth2/failure')
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (View $layout) {
$page = new View(__DIR__.'/../../views/home/auth/oauth2.phtml');
$page = new View(__DIR__ . '/../../views/home/auth/oauth2.phtml');
$layout
->setParam('title', APP_NAME)
@@ -225,17 +214,16 @@ App::get('/error/:code')
->label('scope', 'home')
->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false)
->inject('layout')
->action(function ($code, $layout) {
/** @var Appwrite\Utopia\View $layout */
->action(function (int $code, View $layout) {
$page = new View(__DIR__.'/../../views/error.phtml');
$page = new View(__DIR__ . '/../../views/error.phtml');
$page
->setParam('code', $code)
;
$layout
->setParam('title', 'Error'.' - '.APP_NAME)
->setParam('title', 'Error' . ' - ' . APP_NAME)
->setParam('body', $page);
});
@@ -244,8 +232,7 @@ App::get('/versions')
->groups(['web', 'home'])
->label('scope', 'public')
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
->action(function (Response $response) {
$platforms = Config::getParam('platforms');
@@ -253,15 +240,15 @@ App::get('/versions')
'server' => APP_VERSION_STABLE,
];
foreach($platforms as $platform) {
foreach ($platforms as $platform) {
$languages = $platform['languages'] ?? [];
foreach ($languages as $key => $language) {
if(isset($language['dev']) && $language['dev']) {
if (isset($language['dev']) && $language['dev']) {
continue;
}
if(isset($language['enabled']) && !$language['enabled']) {
if (isset($language['enabled']) && !$language['enabled']) {
continue;
}
-90
View File
@@ -1,90 +0,0 @@
CREATE DATABASE IF NOT EXISTS `appwrite` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `appwrite`;
CREATE TABLE IF NOT EXISTS `template.abuse.abuse` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`_key` varchar(255) NOT NULL,
`_time` int(11) NOT NULL,
`_count` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `unique1` (`_key`,`_time`),
KEY `index1` (`_key`,`_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `template.audit.audit` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` varchar(45) NOT NULL,
`event` varchar(45) NOT NULL,
`resource` varchar(45) DEFAULT NULL,
`userAgent` text NOT NULL,
`ip` varchar(45) NOT NULL,
`location` varchar(45) DEFAULT NULL,
`time` datetime NOT NULL,
`data` longtext DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `index_1` (`userId`),
KEY `index_2` (`event`),
KEY `index_3` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `template.database.documents` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique ID for each node',
`uid` varchar(45) DEFAULT NULL,
`status` int(11) NOT NULL DEFAULT 0,
`createdAt` datetime DEFAULT NULL,
`updatedAt` datetime DEFAULT NULL,
`signature` varchar(32) NOT NULL,
`revision` varchar(45) NOT NULL,
`permissions` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
UNIQUE KEY `index2` (`uid`),
KEY `index3` (`signature`,`uid`,`revision`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `template.database.properties` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`documentUid` varchar(45) NOT NULL COMMENT 'Unique UID foreign key',
`documentRevision` varchar(45) NOT NULL,
`key` varchar(32) NOT NULL COMMENT 'Property key name',
`value` text NOT NULL COMMENT 'Value of property',
`primitive` varchar(32) NOT NULL COMMENT 'Primitive type of property value',
`array` tinyint(4) NOT NULL DEFAULT 0,
`order` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `index1` (`documentUid`),
KEY `index2` (`key`,`value`(5)),
FULLTEXT KEY `index3` (`value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `template.database.relationships` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`revision` varchar(45) NOT NULL,
`start` varchar(45) NOT NULL COMMENT 'Unique UID foreign key',
`end` varchar(45) NOT NULL COMMENT 'Unique UID foreign key',
`key` varchar(256) NOT NULL,
`path` int(11) NOT NULL DEFAULT 0,
`array` tinyint(4) NOT NULL DEFAULT 0,
`order` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `relationships_start_nodes_id_idx` (`start`),
KEY `relationships_end_nodes_id_idx` (`end`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `template.database.unique` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index1` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/* Default App */
CREATE TABLE IF NOT EXISTS `app_console.database.documents` LIKE `template.database.documents`;
CREATE TABLE IF NOT EXISTS `app_console.database.properties` LIKE `template.database.properties`;
CREATE TABLE IF NOT EXISTS `app_console.database.relationships` LIKE `template.database.relationships`;
CREATE TABLE IF NOT EXISTS `app_console.database.unique` LIKE `template.database.unique`;
CREATE TABLE IF NOT EXISTS `app_console.audit.audit` LIKE `template.audit.audit`;
CREATE TABLE IF NOT EXISTS `app_console.abuse.abuse` LIKE `template.abuse.abuse`;
+109 -96
View File
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Appwrite\Runtimes\Runtimes;
@@ -17,7 +18,10 @@ use Utopia\Orchestration\Adapter\DockerCLI;
use Utopia\Orchestration\Orchestration;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\Backblaze;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Device\Linode;
use Utopia\Storage\Device\Wasabi;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Storage;
use Utopia\Swoole\Request;
@@ -35,7 +39,7 @@ Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
const MAINTENANCE_INTERVAL = 3600; // 3600 seconds = 1 hour
/**
* Create a Swoole table to store runtime information
* Create a Swoole table to store runtime information
*/
$activeRuntimes = new Swoole\Table(1024);
$activeRuntimes->column('id', Swoole\Table::TYPE_STRING, 256);
@@ -64,8 +68,8 @@ $providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
$logger = null;
if(!empty($providerName) && !empty($providerConfig) && Logger::hasProvider($providerName)) {
$classname = '\\Utopia\\Logger\\Adapter\\'.\ucfirst($providerName);
if (!empty($providerName) && !empty($providerConfig) && Logger::hasProvider($providerName)) {
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName);
$adapter = new $classname($providerConfig);
$logger = new Logger($adapter);
}
@@ -86,7 +90,7 @@ function logError(Throwable $error, string $action, Utopia\Route $route = null)
if ($route) {
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('url', $route->getPath());
}
$log->addTag('code', $error->getCode());
@@ -110,11 +114,13 @@ function logError(Throwable $error, string $action, Utopia\Route $route = null)
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
};
}
function getStorageDevice($root): Device {
function getStorageDevice($root): Device
{
switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) {
case Storage::DEVICE_LOCAL:default:
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
case Storage::DEVICE_S3:
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
@@ -130,6 +136,27 @@ function getStorageDevice($root): Device {
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
$doSpacesAcl = 'private';
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
case Storage::DEVICE_LINODE:
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
case Storage::DEVICE_WASABI:
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
}
}
@@ -138,13 +165,13 @@ App::post('/v1/runtimes')
->param('runtimeId', '', new Text(64), 'Unique runtime ID.')
->param('source', '', new Text(0), 'Path to source files.')
->param('destination', '', new Text(0), 'Destination folder to store build files into.', true)
->param('vars', [], new Assoc(), 'Environment Variables required for the build')
->param('commands', [], new ArrayList(new Text(0)), 'Commands required to build the container')
->param('runtime', '', new Text(128), 'Runtime for the cloud function')
->param('baseImage', '', new Text(128), 'Base image name of the runtime')
->param('entrypoint', '', new Text(256), 'Entrypoint of the code file', true)
->param('remove', false, new Boolean(), 'Remove a runtime after execution')
->param('workdir', '', new Text(256), 'Working directory', true)
->param('vars', [], new Assoc(), 'Environment Variables required for the build.')
->param('commands', [], new ArrayList(new Text(1024), 100), 'Commands required to build the container. Maximum of 100 commands are allowed, each 1024 characters long.')
->param('runtime', '', new Text(128), 'Runtime for the cloud function.')
->param('baseImage', '', new Text(128), 'Base image name of the runtime.')
->param('entrypoint', '', new Text(256), 'Entrypoint of the code file.', true)
->param('remove', false, new Boolean(), 'Remove a runtime after execution.')
->param('workdir', '', new Text(256), 'Working directory.', true)
->inject('orchestrationPool')
->inject('activeRuntimes')
->inject('response')
@@ -163,9 +190,9 @@ App::post('/v1/runtimes')
try {
Console::info('Building container : ' . $runtimeId);
/**
* Temporary file paths in the executor
/**
* Temporary file paths in the executor
*/
$tmpSource = "/tmp/$runtimeId/src/code.tar.gz";
$tmpBuild = "/tmp/$runtimeId/builds/code.tar.gz";
@@ -176,7 +203,7 @@ App::post('/v1/runtimes')
$sourceDevice = getStorageDevice("/");
$localDevice = new Local();
$buffer = $sourceDevice->read($source);
if(!$localDevice->write($tmpSource, $buffer)) {
if (!$localDevice->write($tmpSource, $buffer)) {
throw new Exception('Failed to copy source code to temporary directory', 500);
};
@@ -202,7 +229,7 @@ App::post('/v1/runtimes')
->setCpus((int) App::getEnv('_APP_FUNCTIONS_CPUS', 0))
->setMemory((int) App::getEnv('_APP_FUNCTIONS_MEMORY', 0))
->setSwap((int) App::getEnv('_APP_FUNCTIONS_MEMORY_SWAP', 0));
/** Keep the container alive if we have commands to be executed */
$entrypoint = !empty($commands) ? [
'tail',
@@ -224,8 +251,8 @@ App::post('/v1/runtimes')
],
workdir: $workdir,
volumes: [
\dirname($tmpSource). ':/tmp:rw',
\dirname($tmpBuild). ':/usr/code:rw'
\dirname($tmpSource) . ':/tmp:rw',
\dirname($tmpBuild) . ':/usr/code:rw'
]
);
@@ -235,7 +262,7 @@ App::post('/v1/runtimes')
$orchestration->networkConnect($runtimeId, App::getEnv('OPEN_RUNTIMES_NETWORK', 'appwrite_runtimes'));
/**
/**
* Execute any commands if they were provided
*/
if (!empty($commands)) {
@@ -244,7 +271,7 @@ App::post('/v1/runtimes')
command: $commands,
stdout: $stdout,
stderr: $stderr,
timeout: App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)
timeout: App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900)
);
if (!$status) {
@@ -265,7 +292,7 @@ App::post('/v1/runtimes')
$outputPath = $destinationDevice->getPath(\uniqid() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$buffer = $localDevice->read($tmpBuild);
if(!$destinationDevice->write($outputPath, $buffer, $localDevice->getFileMimeType($tmpBuild))) {
if (!$destinationDevice->write($outputPath, $buffer, $localDevice->getFileMimeType($tmpBuild))) {
throw new Exception('Failed to move built code to storage', 500);
};
@@ -279,8 +306,8 @@ App::post('/v1/runtimes')
$endTime = \time();
$container = array_merge($container, [
'status' => 'ready',
'stdout' => \utf8_encode($stdout),
'stderr' => \utf8_encode($stderr),
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
'startTime' => $startTime,
'endTime' => $endTime,
'duration' => $endTime - $startTime,
@@ -298,13 +325,12 @@ App::post('/v1/runtimes')
}
Console::success('Build Stage completed in ' . ($endTime - $startTime) . ' seconds');
} catch (Throwable $th) {
Console::error('Build failed: ' . $th->getMessage() . $stdout);
throw new Exception($th->getMessage() . $stdout, 500);
} finally {
// Container cleanup
if($remove) {
if ($remove) {
if (!empty($containerId)) {
// If container properly created
$orchestration->remove($containerId, true);
@@ -337,7 +363,7 @@ App::get('/v1/runtimes')
->action(function ($activeRuntimes, Response $response) {
$runtimes = [];
foreach($activeRuntimes as $runtime) {
foreach ($activeRuntimes as $runtime) {
$runtimes[] = $runtime;
}
@@ -353,7 +379,7 @@ App::get('/v1/runtimes/:runtimeId')
->inject('response')
->action(function ($runtimeId, $activeRuntimes, Response $response) {
if(!$activeRuntimes->exists($runtimeId)) {
if (!$activeRuntimes->exists($runtimeId)) {
throw new Exception('Runtime not found', 404);
}
@@ -372,7 +398,7 @@ App::delete('/v1/runtimes/:runtimeId')
->inject('response')
->action(function (string $runtimeId, $orchestrationPool, $activeRuntimes, Response $response) {
if(!$activeRuntimes->exists($runtimeId)) {
if (!$activeRuntimes->exists($runtimeId)) {
throw new Exception('Runtime not found', 404);
}
@@ -406,9 +432,9 @@ App::delete('/v1/runtimes/:runtimeId')
App::post('/v1/execution')
->desc('Create an execution')
->param('runtimeId', '', new Text(64), 'The runtimeID to execute')
->param('vars', [], new Assoc(), 'Environment variables required for the build')
->param('data', '{}', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
->inject('activeRuntimes')
->inject('response')
@@ -426,7 +452,7 @@ App::post('/v1/execution')
}
Console::info('Executing Runtime: ' . $runtimeId);
$execution = [];
$executionStart = \microtime(true);
$stdout = '';
@@ -449,71 +475,59 @@ App::post('/v1/execution')
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
\curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
'x-internal-challenge: ' . $secret,
'host: null'
]);
$executorResponse = \curl_exec($ch);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
// If timeout error
if (in_array($errNo, [CURLE_OPERATION_TIMEDOUT, 110])) {
$statusCode = 124;
switch (true) {
/** No Error. */
case $errNo === 0:
break;
/** Runtime not ready for requests yet. 111 is the swoole error code for Connection Refused - see https://openswoole.com/docs/swoole-error-code */
case $errNo === 111:
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 406);
/** Any other CURL error */
default:
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 500);
}
// 110 is the Swoole error code for timeout, see: https://www.swoole.co.uk/docs/swoole-error-code
if ($errNo !== 0 && $errNo !== CURLE_COULDNT_CONNECT && $errNo !== CURLE_OPERATION_TIMEDOUT && $errNo !== 110) {
throw new Exception('An internal curl error has occurred within the executor! Error Msg: ' . $error, 406);
switch (true) {
case $statusCode >= 500:
$stderr = $executorResponse ?? 'Internal Runtime error.';
break;
case $statusCode >= 100:
$stdout = $executorResponse;
break;
default:
$stderr = $executorResponse ?? 'Execution failed.';
break;
}
$executionData = [];
if (!empty($executorResponse)) {
$executionData = json_decode($executorResponse, true);
}
if (isset($executionData['code'])) {
$statusCode = $executionData['code'];
}
if ($statusCode === 500) {
if (isset($executionData['message'])) {
$stderr = $executionData['message'];
} else {
$stderr = 'Internal Runtime error';
}
} else if ($statusCode === 124) {
$stderr = 'Execution timed out.';
} else if ($statusCode === 0) {
$stderr = 'Execution failed.';
} else if ($statusCode >= 200 && $statusCode < 300) {
$stdout = $executorResponse;
} else {
$stderr = 'Execution failed.';
}
$executionEnd = \microtime(true);
$executionTime = ($executionEnd - $executionStart);
$functionStatus = ($statusCode >= 200 && $statusCode < 300) ? 'completed' : 'failed';
$functionStatus = ($statusCode >= 500) ? 'failed' : 'completed';
Console::success('Function executed in ' . $executionTime . ' seconds, status: ' . $functionStatus);
$execution = [
'status' => $functionStatus,
'statusCode' => $statusCode,
'stdout' => \utf8_encode(\mb_substr($stdout, -16384)),
'stderr' => \utf8_encode(\mb_substr($stderr, -16384)),
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
'time' => $executionTime,
];
@@ -539,7 +553,7 @@ App::setResource('activeRuntimes', fn() => $activeRuntimes);
App::error(function ($utopia, $error, $request, $response) {
$route = $utopia->match($request);
logError($error, "httpError", $route);
switch ($error->getCode()) {
case 400: // Error allowed publicly
case 401: // Error allowed publicly
@@ -558,7 +572,7 @@ App::error(function ($utopia, $error, $request, $response) {
default:
$code = 500; // All other errors get the generic 500 server error status code
}
$output = [
'message' => $error->getMessage(),
'code' => $error->getCode(),
@@ -579,24 +593,24 @@ App::error(function ($utopia, $error, $request, $response) {
App::init(function ($request, $response) {
$secretKey = $request->getHeader('x-appwrite-executor-key', '');
if (empty($secretKey)) {
throw new Exception('Missing executor key', 401);
}
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
if (empty($secretKey)) {
throw new Exception('Missing executor key', 401);
}
}
if ($secretKey !== App::getEnv('_APP_EXECUTOR_SECRET', '')) {
throw new Exception('Missing executor key', 401);
}
}, ['request', 'response']);
$http->on('start', function ($http) {
global $orchestrationPool;
global $activeRuntimes;
/**
/**
* Warmup: make sure images are ready to run fast 🚀
*/
$runtimes = new Runtimes();
$runtimes = new Runtimes('v1');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
$runtimes = $runtimes->getAll(true, $allowList);
foreach ($runtimes as $runtime) {
@@ -684,11 +698,10 @@ $http->on('start', function ($http) {
}
}
});
});
$http->on('beforeShutdown', function() {
$http->on('beforeShutdown', function () {
global $orchestrationPool;
Console::info('Cleaning up containers before shutdown...');
@@ -697,7 +710,7 @@ $http->on('beforeShutdown', function() {
$orchestrationPool->put($orchestration);
foreach ($functionsToRemove as $container) {
go(function () use ($orchestrationPool, $container) {
go(function () use ($orchestrationPool, $container) {
try {
$orchestration = $orchestrationPool->get();
$orchestration->remove($container->getId(), true);
@@ -723,7 +736,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
logError($th, "serverError");
$swooleResponse->setStatusCode(500);
$output = [
'message' => 'Error: '. $th->getMessage(),
'message' => 'Error: ' . $th->getMessage(),
'code' => 500,
'file' => $th->getFile(),
'line' => $th->getLine(),
@@ -733,4 +746,4 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
});
$http->start();
$http->start();
+34 -35
View File
@@ -1,6 +1,6 @@
<?php
require_once __DIR__.'/../vendor/autoload.php';
require_once __DIR__ . '/../vendor/autoload.php';
use Appwrite\Utopia\Response;
use Swoole\Process;
@@ -38,15 +38,15 @@ $http
])
;
$http->on('WorkerStart', function($server, $workerId) {
Console::success('Worker '.++$workerId.' started successfully');
$http->on('WorkerStart', function ($server, $workerId) {
Console::success('Worker ' . ++$workerId . ' started successfully');
});
$http->on('BeforeReload', function($server, $workerId) {
$http->on('BeforeReload', function ($server, $workerId) {
Console::success('Starting reload...');
});
$http->on('AfterReload', function($server, $workerId) {
$http->on('AfterReload', function ($server, $workerId) {
Console::success('Reload completed...');
});
@@ -57,7 +57,7 @@ include __DIR__ . '/controllers/general.php';
$http->on('start', function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
go(function() use ($register, $app) {
go(function () use ($register, $app) {
// wait for database to be ready
$attempts = 0;
$max = 10;
@@ -69,10 +69,10 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
break; // leave the do-while if successful
} catch(\Exception $e) {
} catch (\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: '. $e->getMessage());
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
@@ -86,7 +86,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
Console::success('[Setup] - Server database init started...');
$collections = Config::getParam('collections', []); /** @var array $collections */
if(!$dbForConsole->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'))) {
if (!$dbForConsole->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'))) {
$redis->flushAll();
Console::success('[Setup] - Creating database: appwrite...');
@@ -101,7 +101,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
Console::success('[Setup] - Skip: metadata table already exists');
}
if($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
$audit->setup();
}
@@ -112,10 +112,10 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
}
foreach ($collections as $key => $collection) {
if(($collection['$collection'] ?? '') !== Database::METADATA) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if(!$dbForConsole->getCollection($key)->isEmpty()) {
if (!$dbForConsole->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
@@ -150,7 +150,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
$dbForConsole->createCollection($key, $attributes, $indexes);
}
if($dbForConsole->getDocument('buckets', 'default')->isEmpty()) {
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty()) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
'$id' => 'default',
@@ -170,16 +170,16 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['files'] ?? [];
if(empty($files)) {
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
@@ -193,7 +193,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
'format' => $attribute['format'] ?? ''
]);
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
@@ -203,15 +203,14 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
Console::success('[Setup] - Server database init completed...');
});
Console::success('Server started successfully (max payload is '.number_format($payloadSize).' bytes)');
Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)');
Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}");
// listen ctrl + c
@@ -225,13 +224,13 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
if(Files::isFileLoaded($request->getURI())) {
if (Files::isFileLoaded($request->getURI())) {
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
$response
->setContentType(Files::getFileMimeType($request->getURI()))
->addHeader('Cache-Control', 'public, max-age='.$time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache
->addHeader('Cache-Control', 'public, max-age=' . $time)
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache
->send(Files::getFileContents($request->getURI()))
;
@@ -255,11 +254,11 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->getResource("logger");
if($logger) {
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
} catch(\Throwable $_th) {
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
@@ -268,7 +267,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$log = new Utopia\Logger\Log();
if(isset($user) && !$user->isEmpty()) {
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
}
@@ -279,7 +278,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$log->setMessage($th->getMessage());
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('url', $route->getPath());
$log->addTag('verboseType', get_class($th));
$log->addTag('code', $th->getCode());
// $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant
@@ -298,18 +297,18 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
foreach($loggerBreadcrumbs as $loggerBreadcrumb) {
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
$log->addBreadcrumb($loggerBreadcrumb);
}
$responseCode = $logger->addLog($log);
Console::info('Log pushed with status code: '.$responseCode);
Console::info('Log pushed with status code: ' . $responseCode);
}
Console::error('[Error] Type: '.get_class($th));
Console::error('[Error] Message: '.$th->getMessage());
Console::error('[Error] File: '.$th->getFile());
Console::error('[Error] Line: '.$th->getLine());
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
/**
* Reset Database connection if PDOException was thrown.
@@ -321,7 +320,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$swooleResponse->setStatusCode(500);
$output = ((App::isDevelopment())) ? [
'message' => 'Error: '. $th->getMessage(),
'message' => 'Error: ' . $th->getMessage(),
'code' => 500,
'file' => $th->getFile(),
'line' => $th->getLine(),
+314 -207
View File
@@ -2,16 +2,17 @@
/**
* Init
*
*
* Initializes both Appwrite API entry point, queue workers, and CLI tasks.
* Set configuration, framework resources & app constants
*
*
*/
if (\file_exists(__DIR__.'/../vendor/autoload.php')) {
require_once __DIR__.'/../vendor/autoload.php';
if (\file_exists(__DIR__ . '/../vendor/autoload.php')) {
require_once __DIR__ . '/../vendor/autoload.php';
}
ini_set('memory_limit','512M');
ini_set('memory_limit', '512M');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
ini_set('default_socket_timeout', -1);
@@ -22,7 +23,11 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
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;
use Appwrite\GraphQL\Builder;
use Appwrite\GraphQL\CoroutinePromiseAdapter;
use Appwrite\Network\Validator\Email;
@@ -55,15 +60,18 @@ use Swoole\Database\RedisPool;
use Utopia\Database\Query;
use Utopia\Storage\Device;
use Utopia\Storage\Storage;
use Utopia\Storage\Device\Backblaze;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Device\Linode;
use Utopia\Storage\Device\Wasabi;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
const APP_EMAIL_SECURITY = ''; // Default security email address
const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s';
const APP_USERAGENT = APP_NAME . '-Server v%s. Please report abuse at %s';
const APP_MODE_DEFAULT = 'default';
const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
@@ -72,8 +80,10 @@ const APP_LIMIT_USERS = 10000;
const APP_LIMIT_ANTIVIRUS = 20000000; //20MB
const APP_LIMIT_ENCRYPTION = 20000000; //20MB
const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_CACHE_BUSTER = 304;
const APP_VERSION_STABLE = '0.13.4';
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_SUBQUERY = 1000;
const APP_CACHE_BUSTER = 305;
const APP_VERSION_STABLE = '0.14.2';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -97,7 +107,7 @@ const APP_SOCIAL_GITHUB = 'https://github.com/appwrite';
const APP_SOCIAL_DISCORD = 'https://appwrite.io/discord';
const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
@@ -117,7 +127,7 @@ const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_FUNCTIONS = 'functions';
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
const DELETE_TYPE_USERS = 'users';
const DELETE_TYPE_TEAMS= 'teams';
const DELETE_TYPE_TEAMS = 'teams';
const DELETE_TYPE_EXECUTIONS = 'executions';
const DELETE_TYPE_AUDIT = 'audit';
const DELETE_TYPE_ABUSE = 'abuse';
@@ -130,13 +140,14 @@ const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
const MAIL_TYPE_RECOVERY = 'recovery';
const MAIL_TYPE_INVITATION = 'invitation';
const MAIL_TYPE_CERTIFICATE = 'certificate';
// Auth Types
const APP_AUTH_TYPE_SESSION = 'Session';
const APP_AUTH_TYPE_JWT = 'JWT';
const APP_AUTH_TYPE_KEY = 'Key';
const APP_AUTH_TYPE_ADMIN = 'Admin';
// Response related
const MAX_OUTPUT_CHUNK_SIZE = 2*1024*1024; // 2MB
const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
$register = new Registry();
@@ -145,82 +156,89 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
/*
* ENV vars
*/
Config::load('events', __DIR__.'/config/events.php');
Config::load('auth', __DIR__.'/config/auth.php');
Config::load('errors', __DIR__.'/config/errors.php');
Config::load('providers', __DIR__.'/config/providers.php');
Config::load('platforms', __DIR__.'/config/platforms.php');
Config::load('collections', __DIR__.'/config/collections.php');
Config::load('runtimes', __DIR__.'/config/runtimes.php');
Config::load('roles', __DIR__.'/config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__.'/config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__.'/config/services.php'); // List of services
Config::load('variables', __DIR__.'/config/variables.php'); // List of env variables
Config::load('avatar-browsers', __DIR__.'/config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__.'/config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__.'/config/avatars/flags.php');
Config::load('locale-codes', __DIR__.'/config/locale/codes.php');
Config::load('locale-currencies', __DIR__.'/config/locale/currencies.php');
Config::load('locale-eu', __DIR__.'/config/locale/eu.php');
Config::load('locale-languages', __DIR__.'/config/locale/languages.php');
Config::load('locale-phones', __DIR__.'/config/locale/phones.php');
Config::load('locale-countries', __DIR__.'/config/locale/countries.php');
Config::load('locale-continents', __DIR__.'/config/locale/continents.php');
Config::load('storage-logos', __DIR__.'/config/storage/logos.php');
Config::load('storage-mimes', __DIR__.'/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__.'/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php');
Config::load('events', __DIR__ . '/config/events.php');
Config::load('auth', __DIR__ . '/config/auth.php');
Config::load('errors', __DIR__ . '/config/errors.php');
Config::load('providers', __DIR__ . '/config/providers.php');
Config::load('platforms', __DIR__ . '/config/platforms.php');
Config::load('collections', __DIR__ . '/config/collections.php');
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
Config::load('roles', __DIR__ . '/config/roles.php'); // User roles and scopes
Config::load('scopes', __DIR__ . '/config/scopes.php'); // User roles and scopes
Config::load('services', __DIR__ . '/config/services.php'); // List of services
Config::load('variables', __DIR__ . '/config/variables.php'); // List of env variables
Config::load('avatar-browsers', __DIR__ . '/config/avatars/browsers.php');
Config::load('avatar-credit-cards', __DIR__ . '/config/avatars/credit-cards.php');
Config::load('avatar-flags', __DIR__ . '/config/avatars/flags.php');
Config::load('locale-codes', __DIR__ . '/config/locale/codes.php');
Config::load('locale-currencies', __DIR__ . '/config/locale/currencies.php');
Config::load('locale-eu', __DIR__ . '/config/locale/eu.php');
Config::load('locale-languages', __DIR__ . '/config/locale/languages.php');
Config::load('locale-phones', __DIR__ . '/config/locale/phones.php');
Config::load('locale-countries', __DIR__ . '/config/locale/countries.php');
Config::load('locale-continents', __DIR__ . '/config/locale/continents.php');
Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
$user = App::getEnv('_APP_REDIS_USER','');
$pass = App::getEnv('_APP_REDIS_PASS','');
if(!empty($user) || !empty($pass)) {
Resque::setBackend('redis://'.$user.':'.$pass.'@'.App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
$user = App::getEnv('_APP_REDIS_USER', '');
$pass = App::getEnv('_APP_REDIS_PASS', '');
if (!empty($user) || !empty($pass)) {
Resque::setBackend('redis://' . $user . ':' . $pass . '@' . App::getEnv('_APP_REDIS_HOST', '') . ':' . App::getEnv('_APP_REDIS_PORT', ''));
} else {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '') . ':' . App::getEnv('_APP_REDIS_PORT', ''));
}
/**
* New DB Filters
*/
Database::addFilter('casting',
function($value) {
return json_encode(['value' => $value]);
Database::addFilter(
'casting',
function (mixed $value) {
return json_encode(['value' => $value], JSON_PRESERVE_ZERO_FRACTION);
},
function($value) {
function (mixed $value) {
if (is_null($value)) {
return null;
}
return json_decode($value, true)['value'];
}
);
Database::addFilter('enum',
function($value, Document $attribute) {
Database::addFilter(
'enum',
function (mixed $value, Document $attribute) {
if ($attribute->isSet('elements')) {
$attribute->removeAttribute('elements');
}
return $value;
},
function($value, Document $attribute) {
function (mixed $value, Document $attribute) {
$formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true);
if (isset($formatOptions['elements'])) {
$attribute->setAttribute('elements', $formatOptions['elements']);
}
return $value;
}
);
Database::addFilter('range',
function($value, Document $attribute) {
Database::addFilter(
'range',
function (mixed $value, Document $attribute) {
if ($attribute->isSet('min')) {
$attribute->removeAttribute('min');
}
if ($attribute->isSet('max')) {
$attribute->removeAttribute('max');
}
return $value;
},
function($value, Document $attribute) {
function (mixed $value, Document $attribute) {
$formatOptions = json_decode($attribute->getAttribute('formatOptions', '[]'), true);
if (isset($formatOptions['min']) || isset($formatOptions['max'])) {
$attribute
@@ -228,87 +246,134 @@ Database::addFilter('range',
->setAttribute('max', $formatOptions['max'])
;
}
return $value;
}
);
Database::addFilter('subQueryAttributes',
function($value) {
Database::addFilter(
'subQueryAttributes',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getAttributeLimit(), 0, []);
], $database->getAttributeLimit());
}
);
Database::addFilter('subQueryIndexes',
function($value) {
Database::addFilter(
'subQueryIndexes',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('indexes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], 64, 0, []);
], 64);
}
);
Database::addFilter('subQueryPlatforms',
function($value) {
Database::addFilter(
'subQueryPlatforms',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
], APP_LIMIT_SUBQUERY);
}
);
Database::addFilter('subQueryDomains',
function($value) {
Database::addFilter(
'subQueryDomains',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
], APP_LIMIT_SUBQUERY);
}
);
Database::addFilter('subQueryKeys',
function($value) {
Database::addFilter(
'subQueryKeys',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
], APP_LIMIT_SUBQUERY);
}
);
Database::addFilter('subQueryWebhooks',
function($value) {
Database::addFilter(
'subQueryWebhooks',
function (mixed $value) {
return null;
},
function($value, Document $document, Database $database) {
function (mixed $value, Document $document, Database $database) {
return $database
->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
], APP_LIMIT_SUBQUERY);
}
);
Database::addFilter('encrypt',
function($value) {
Database::addFilter(
'subQuerySessions',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database->find('sessions', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
], APP_LIMIT_SUBQUERY));
}
);
Database::addFilter(
'subQueryTokens',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('tokens', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
], APP_LIMIT_SUBQUERY));
}
);
Database::addFilter(
'subQueryMemberships',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('memberships', [
new Query('userId', Query::TYPE_EQUAL, [$document->getId()])
], APP_LIMIT_SUBQUERY));
}
);
Database::addFilter(
'encrypt',
function (mixed $value) {
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
return json_encode([
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
@@ -317,12 +382,12 @@ Database::addFilter('encrypt',
'version' => '1',
]);
},
function($value) {
if(is_null($value)) {
function (mixed $value) {
if (is_null($value)) {
return null;
}
$value = json_decode($value, true);
$key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $value['version']);
return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
}
@@ -331,30 +396,30 @@ Database::addFilter('encrypt',
/**
* DB Formats
*/
Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () {
return new Email();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) {
$elements = $attribute['formatOptions']['elements'];
return new WhiteList($elements, true);
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function () {
return new IP();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function () {
return new URL();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_FLOAT);
@@ -363,30 +428,33 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) {
/*
* Registry
*/
$register->set('logger', function () { // Register error logger
$register->set('logger', function () {
// Register error logger
$providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
if(empty($providerName) || empty($providerConfig)) {
if (empty($providerName) || empty($providerConfig)) {
return null;
}
if(!Logger::hasProvider($providerName)) {
if (!Logger::hasProvider($providerName)) {
throw new Exception("Logging provider not supported. Logging disabled.", 500, Exception::GENERAL_SERVER_ERROR);
}
$classname = '\\Utopia\\Logger\\Adapter\\'.\ucfirst($providerName);
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName);
$adapter = new $classname($providerConfig);
return new Logger($adapter);
});
$register->set('dbPool', function () { // Register DB connection
$register->set('dbPool', function () {
// Register DB connection
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
$dbPass = App::getEnv('_APP_DB_PASS', '');
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pool = new PDOPool((new PDOConfig())
$pool = new PDOPool(
(new PDOConfig())
->withHost($dbHost)
->withPort($dbPort)
->withDbName($dbScheme)
@@ -395,8 +463,9 @@ $register->set('dbPool', function () { // Register DB connection
->withPassword($dbPass)
->withOptions([
PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed
])
, 64);
]),
64
);
return $pool;
});
@@ -408,19 +477,22 @@ $register->set('redisPool', function () {
$redisAuth = '';
if ($redisUser && $redisPass) {
$redisAuth = $redisUser.':'.$redisPass;
$redisAuth = $redisUser . ':' . $redisPass;
}
$pool = new RedisPool((new RedisConfig)
$pool = new RedisPool(
(new RedisConfig())
->withHost($redisHost)
->withPort($redisPort)
->withAuth($redisAuth)
->withDbIndex(0)
, 64);
->withDbIndex(0),
64
);
return $pool;
});
$register->set('influxdb', function () { // Register DB connection
$register->set('influxdb', function () {
// Register DB connection
$host = App::getEnv('_APP_INFLUXDB_HOST', '');
$port = App::getEnv('_APP_INFLUXDB_PORT', '');
@@ -433,7 +505,8 @@ $register->set('influxdb', function () { // Register DB connection
return $client;
});
$register->set('statsd', function () { // Register DB connection
$register->set('statsd', function () {
// Register DB connection
$host = App::getEnv('_APP_STATSD_HOST', 'telegraf');
$port = App::getEnv('_APP_STATSD_PORT', 8125);
@@ -460,7 +533,7 @@ $register->set('smtp', function () {
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$from = \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server'));
$from = \urldecode(App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$mail->setFrom($email, $from);
@@ -471,9 +544,10 @@ $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-03.mmdb');
});
$register->set('db', function () { // This is usually for our workers or CLI commands scope
$register->set('db', function () {
// This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
@@ -490,7 +564,8 @@ $register->set('db', function () { // This is usually for our workers or CLI com
return $pdo;
});
$register->set('cache', function () { // This is usually for our workers or CLI commands scope
$register->set('cache', function () {
// This is usually for our workers or CLI commands scope
$redis = new Redis();
$redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
@@ -505,59 +580,59 @@ $register->set('promiseAdapter', function () {
* Localization
*/
Locale::$exceptions = false;
Locale::setLanguageFromJSON('af', __DIR__.'/config/locale/translations/af.json');
Locale::setLanguageFromJSON('ar', __DIR__.'/config/locale/translations/ar.json');
Locale::setLanguageFromJSON('as', __DIR__.'/config/locale/translations/as.json');
Locale::setLanguageFromJSON('az', __DIR__.'/config/locale/translations/az.json');
Locale::setLanguageFromJSON('be', __DIR__.'/config/locale/translations/be.json');
Locale::setLanguageFromJSON('bg', __DIR__.'/config/locale/translations/bg.json');
Locale::setLanguageFromJSON('bh', __DIR__.'/config/locale/translations/bh.json');
Locale::setLanguageFromJSON('bn', __DIR__.'/config/locale/translations/bn.json');
Locale::setLanguageFromJSON('bs', __DIR__.'/config/locale/translations/bs.json');
Locale::setLanguageFromJSON('ca', __DIR__.'/config/locale/translations/ca.json');
Locale::setLanguageFromJSON('cs', __DIR__.'/config/locale/translations/cs.json');
Locale::setLanguageFromJSON('da', __DIR__.'/config/locale/translations/da.json');
Locale::setLanguageFromJSON('de', __DIR__.'/config/locale/translations/de.json');
Locale::setLanguageFromJSON('el', __DIR__.'/config/locale/translations/el.json');
Locale::setLanguageFromJSON('en', __DIR__.'/config/locale/translations/en.json');
Locale::setLanguageFromJSON('eo', __DIR__.'/config/locale/translations/eo.json');
Locale::setLanguageFromJSON('es', __DIR__.'/config/locale/translations/es.json');
Locale::setLanguageFromJSON('fa', __DIR__.'/config/locale/translations/fa.json');
Locale::setLanguageFromJSON('fi', __DIR__.'/config/locale/translations/fi.json');
Locale::setLanguageFromJSON('fo', __DIR__.'/config/locale/translations/fo.json');
Locale::setLanguageFromJSON('fr', __DIR__.'/config/locale/translations/fr.json');
Locale::setLanguageFromJSON('ga', __DIR__.'/config/locale/translations/ga.json');
Locale::setLanguageFromJSON('gu', __DIR__.'/config/locale/translations/gu.json');
Locale::setLanguageFromJSON('he', __DIR__.'/config/locale/translations/he.json');
Locale::setLanguageFromJSON('hi', __DIR__.'/config/locale/translations/hi.json');
Locale::setLanguageFromJSON('hr', __DIR__.'/config/locale/translations/hr.json');
Locale::setLanguageFromJSON('hu', __DIR__.'/config/locale/translations/hu.json');
Locale::setLanguageFromJSON('hy', __DIR__.'/config/locale/translations/hy.json');
Locale::setLanguageFromJSON('id', __DIR__.'/config/locale/translations/id.json');
Locale::setLanguageFromJSON('is', __DIR__.'/config/locale/translations/is.json');
Locale::setLanguageFromJSON('it', __DIR__.'/config/locale/translations/it.json');
Locale::setLanguageFromJSON('ja', __DIR__.'/config/locale/translations/ja.json');
Locale::setLanguageFromJSON('jv', __DIR__.'/config/locale/translations/jv.json');
Locale::setLanguageFromJSON('kn', __DIR__.'/config/locale/translations/kn.json');
Locale::setLanguageFromJSON('km', __DIR__.'/config/locale/translations/km.json');
Locale::setLanguageFromJSON('ko', __DIR__.'/config/locale/translations/ko.json');
Locale::setLanguageFromJSON('la', __DIR__.'/config/locale/translations/la.json');
Locale::setLanguageFromJSON('lb', __DIR__.'/config/locale/translations/lb.json');
Locale::setLanguageFromJSON('lt', __DIR__.'/config/locale/translations/lt.json');
Locale::setLanguageFromJSON('lv', __DIR__.'/config/locale/translations/lv.json');
Locale::setLanguageFromJSON('ml', __DIR__.'/config/locale/translations/ml.json');
Locale::setLanguageFromJSON('mr', __DIR__.'/config/locale/translations/mr.json');
Locale::setLanguageFromJSON('ms', __DIR__.'/config/locale/translations/ms.json');
Locale::setLanguageFromJSON('nb', __DIR__.'/config/locale/translations/nb.json');
Locale::setLanguageFromJSON('ne', __DIR__.'/config/locale/translations/ne.json');
Locale::setLanguageFromJSON('nl', __DIR__.'/config/locale/translations/nl.json');
Locale::setLanguageFromJSON('nn', __DIR__.'/config/locale/translations/nn.json');
Locale::setLanguageFromJSON('or', __DIR__.'/config/locale/translations/or.json');
Locale::setLanguageFromJSON('pa', __DIR__.'/config/locale/translations/pa.json');
Locale::setLanguageFromJSON('pl', __DIR__.'/config/locale/translations/pl.json');
Locale::setLanguageFromJSON('pt-br', __DIR__.'/config/locale/translations/pt-br.json');
Locale::setLanguageFromJSON('pt-pt', __DIR__.'/config/locale/translations/pt-pt.json');
Locale::setLanguageFromJSON('ro', __DIR__.'/config/locale/translations/ro.json');
Locale::setLanguageFromJSON('af', __DIR__ . '/config/locale/translations/af.json');
Locale::setLanguageFromJSON('ar', __DIR__ . '/config/locale/translations/ar.json');
Locale::setLanguageFromJSON('as', __DIR__ . '/config/locale/translations/as.json');
Locale::setLanguageFromJSON('az', __DIR__ . '/config/locale/translations/az.json');
Locale::setLanguageFromJSON('be', __DIR__ . '/config/locale/translations/be.json');
Locale::setLanguageFromJSON('bg', __DIR__ . '/config/locale/translations/bg.json');
Locale::setLanguageFromJSON('bh', __DIR__ . '/config/locale/translations/bh.json');
Locale::setLanguageFromJSON('bn', __DIR__ . '/config/locale/translations/bn.json');
Locale::setLanguageFromJSON('bs', __DIR__ . '/config/locale/translations/bs.json');
Locale::setLanguageFromJSON('ca', __DIR__ . '/config/locale/translations/ca.json');
Locale::setLanguageFromJSON('cs', __DIR__ . '/config/locale/translations/cs.json');
Locale::setLanguageFromJSON('da', __DIR__ . '/config/locale/translations/da.json');
Locale::setLanguageFromJSON('de', __DIR__ . '/config/locale/translations/de.json');
Locale::setLanguageFromJSON('el', __DIR__ . '/config/locale/translations/el.json');
Locale::setLanguageFromJSON('en', __DIR__ . '/config/locale/translations/en.json');
Locale::setLanguageFromJSON('eo', __DIR__ . '/config/locale/translations/eo.json');
Locale::setLanguageFromJSON('es', __DIR__ . '/config/locale/translations/es.json');
Locale::setLanguageFromJSON('fa', __DIR__ . '/config/locale/translations/fa.json');
Locale::setLanguageFromJSON('fi', __DIR__ . '/config/locale/translations/fi.json');
Locale::setLanguageFromJSON('fo', __DIR__ . '/config/locale/translations/fo.json');
Locale::setLanguageFromJSON('fr', __DIR__ . '/config/locale/translations/fr.json');
Locale::setLanguageFromJSON('ga', __DIR__ . '/config/locale/translations/ga.json');
Locale::setLanguageFromJSON('gu', __DIR__ . '/config/locale/translations/gu.json');
Locale::setLanguageFromJSON('he', __DIR__ . '/config/locale/translations/he.json');
Locale::setLanguageFromJSON('hi', __DIR__ . '/config/locale/translations/hi.json');
Locale::setLanguageFromJSON('hr', __DIR__ . '/config/locale/translations/hr.json');
Locale::setLanguageFromJSON('hu', __DIR__ . '/config/locale/translations/hu.json');
Locale::setLanguageFromJSON('hy', __DIR__ . '/config/locale/translations/hy.json');
Locale::setLanguageFromJSON('id', __DIR__ . '/config/locale/translations/id.json');
Locale::setLanguageFromJSON('is', __DIR__ . '/config/locale/translations/is.json');
Locale::setLanguageFromJSON('it', __DIR__ . '/config/locale/translations/it.json');
Locale::setLanguageFromJSON('ja', __DIR__ . '/config/locale/translations/ja.json');
Locale::setLanguageFromJSON('jv', __DIR__ . '/config/locale/translations/jv.json');
Locale::setLanguageFromJSON('kn', __DIR__ . '/config/locale/translations/kn.json');
Locale::setLanguageFromJSON('km', __DIR__ . '/config/locale/translations/km.json');
Locale::setLanguageFromJSON('ko', __DIR__ . '/config/locale/translations/ko.json');
Locale::setLanguageFromJSON('la', __DIR__ . '/config/locale/translations/la.json');
Locale::setLanguageFromJSON('lb', __DIR__ . '/config/locale/translations/lb.json');
Locale::setLanguageFromJSON('lt', __DIR__ . '/config/locale/translations/lt.json');
Locale::setLanguageFromJSON('lv', __DIR__ . '/config/locale/translations/lv.json');
Locale::setLanguageFromJSON('ml', __DIR__ . '/config/locale/translations/ml.json');
Locale::setLanguageFromJSON('mr', __DIR__ . '/config/locale/translations/mr.json');
Locale::setLanguageFromJSON('ms', __DIR__ . '/config/locale/translations/ms.json');
Locale::setLanguageFromJSON('nb', __DIR__ . '/config/locale/translations/nb.json');
Locale::setLanguageFromJSON('ne', __DIR__ . '/config/locale/translations/ne.json');
Locale::setLanguageFromJSON('nl', __DIR__ . '/config/locale/translations/nl.json');
Locale::setLanguageFromJSON('nn', __DIR__ . '/config/locale/translations/nn.json');
Locale::setLanguageFromJSON('or', __DIR__ . '/config/locale/translations/or.json');
Locale::setLanguageFromJSON('pa', __DIR__ . '/config/locale/translations/pa.json');
Locale::setLanguageFromJSON('pl', __DIR__ . '/config/locale/translations/pl.json');
Locale::setLanguageFromJSON('pt-br', __DIR__ . '/config/locale/translations/pt-br.json');
Locale::setLanguageFromJSON('pt-pt', __DIR__ . '/config/locale/translations/pt-pt.json');
Locale::setLanguageFromJSON('ro', __DIR__ . '/config/locale/translations/ro.json');
Locale::setLanguageFromJSON('ru', __DIR__ . '/config/locale/translations/ru.json');
Locale::setLanguageFromJSON('sa', __DIR__ . '/config/locale/translations/sa.json');
Locale::setLanguageFromJSON('sd', __DIR__ . '/config/locale/translations/sd.json');
@@ -568,39 +643,41 @@ Locale::setLanguageFromJSON('sn', __DIR__ . '/config/locale/translations/sn.json
Locale::setLanguageFromJSON('sq', __DIR__ . '/config/locale/translations/sq.json');
Locale::setLanguageFromJSON('sv', __DIR__ . '/config/locale/translations/sv.json');
Locale::setLanguageFromJSON('ta', __DIR__ . '/config/locale/translations/ta.json');
Locale::setLanguageFromJSON('te', __DIR__.'/config/locale/translations/te.json');
Locale::setLanguageFromJSON('th', __DIR__.'/config/locale/translations/th.json');
Locale::setLanguageFromJSON('tl', __DIR__.'/config/locale/translations/tl.json');
Locale::setLanguageFromJSON('tr', __DIR__.'/config/locale/translations/tr.json');
Locale::setLanguageFromJSON('uk', __DIR__.'/config/locale/translations/uk.json');
Locale::setLanguageFromJSON('ur', __DIR__.'/config/locale/translations/ur.json');
Locale::setLanguageFromJSON('vi', __DIR__.'/config/locale/translations/vi.json');
Locale::setLanguageFromJSON('zh-cn', __DIR__.'/config/locale/translations/zh-cn.json');
Locale::setLanguageFromJSON('zh-tw', __DIR__.'/config/locale/translations/zh-tw.json');
Locale::setLanguageFromJSON('te', __DIR__ . '/config/locale/translations/te.json');
Locale::setLanguageFromJSON('th', __DIR__ . '/config/locale/translations/th.json');
Locale::setLanguageFromJSON('tl', __DIR__ . '/config/locale/translations/tl.json');
Locale::setLanguageFromJSON('tr', __DIR__ . '/config/locale/translations/tr.json');
Locale::setLanguageFromJSON('uk', __DIR__ . '/config/locale/translations/uk.json');
Locale::setLanguageFromJSON('ur', __DIR__ . '/config/locale/translations/ur.json');
Locale::setLanguageFromJSON('vi', __DIR__ . '/config/locale/translations/vi.json');
Locale::setLanguageFromJSON('zh-cn', __DIR__ . '/config/locale/translations/zh-cn.json');
Locale::setLanguageFromJSON('zh-tw', __DIR__ . '/config/locale/translations/zh-tw.json');
\stream_context_set_default([ // Set global user agent and http settings
'http' => [
'method' => 'GET',
'user_agent' => \sprintf(APP_USERAGENT,
'user_agent' => \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
),
'timeout' => 2,
],
]);
// Runtime Execution
App::setResource('logger', function($register) {
App::setResource('logger', function ($register) {
return $register->get('logger');
}, ['register']);
App::setResource('loggerBreadcrumbs', function() {
App::setResource('loggerBreadcrumbs', function () {
return [];
});
App::setResource('register', fn() => $register);
App::setResource('layout', function($locale) {
$layout = new View(__DIR__.'/views/layouts/default.phtml');
App::setResource('layout', function ($locale) {
$layout = new View(__DIR__ . '/views/layouts/default.phtml');
$layout->setParam('locale', $locale);
return $layout;
@@ -610,11 +687,11 @@ App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')))
// Queues
App::setResource('events', fn() => new Event('', ''));
App::setResource('audits', fn() => new Event(Event::AUDITS_QUEUE_NAME, Event::AUDITS_CLASS_NAME));
App::setResource('mails', fn() => new Event(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME));
App::setResource('deletes', fn() => new Event(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME));
App::setResource('database', fn() => new Event(Event::DATABASE_QUEUE_NAME, Event::DATABASE_CLASS_NAME));
App::setResource('usage', function($register) {
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('usage', function ($register) {
return new Stats($register->get('statsd'));
}, ['register']);
@@ -654,7 +731,7 @@ App::setResource('clients', function ($request, $console, $project) {
return $clients;
}, ['request', 'console', 'project']);
App::setResource('user', function($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
@@ -664,21 +741,28 @@ App::setResource('user', function($mode, $project, $console, $request, $response
Authorization::setDefaultStatus(true);
Auth::setCookieName('a_session_'.$project->getId());
Auth::setCookieName('a_session_' . $project->getId());
if (APP_MODE_ADMIN === $mode) {
Auth::setCookieName('a_session_'.$console->getId());
Auth::setCookieName('a_session_' . $console->getId());
}
$session = Auth::decodeSession(
$request->getCookie(Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName.'_legacy', '')));// Get fallback session from old clients (no SameSite support)
$request->getCookie(
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
)
);// Get fallback session from old clients (no SameSite support)
// Get fallback session from clients who block 3rd-party cookies
if($response) $response->addHeader('X-Debug-Fallback', 'false');
if ($response) {
$response->addHeader('X-Debug-Fallback', 'false');
}
if(empty($session['id']) && empty($session['secret'])) {
if($response) $response->addHeader('X-Debug-Fallback', 'true');
if (empty($session['id']) && empty($session['secret'])) {
if ($response) {
$response->addHeader('X-Debug-Fallback', 'true');
}
$fallback = $request->getHeader('x-fallback-cookies', '');
$fallback = \json_decode($fallback, true);
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
@@ -690,17 +774,17 @@ App::setResource('user', function($mode, $project, $console, $request, $response
if (APP_MODE_ADMIN !== $mode) {
if ($project->isEmpty()) {
$user = new Document(['$id' => '', '$collection' => 'users']);
}
else {
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
else {
} else {
$user = $dbForConsole->getDocument('users', Auth::$unique);
}
if ($user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)) { // Validate user has valid login token
if (
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
) { // Validate user has valid login token
$user = new Document(['$id' => '', '$collection' => 'users']);
}
@@ -720,13 +804,13 @@ App::setResource('user', function($mode, $project, $console, $request, $response
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception('Failed to verify JWT. '.$error->getMessage(), 401, Exception::USER_JWT_INVALID);
throw new Exception('Failed to verify JWT. ' . $error->getMessage(), 401, Exception::USER_JWT_INVALID);
}
$jwtUserId = $payload['userId'] ?? '';
$jwtSessionId = $payload['sessionId'] ?? '';
if($jwtUserId && $jwtSessionId) {
if ($jwtUserId && $jwtSessionId) {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
@@ -738,14 +822,14 @@ App::setResource('user', function($mode, $project, $console, $request, $response
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']);
App::setResource('project', function($dbForConsole, $request, $console) {
App::setResource('project', function ($dbForConsole, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console'));
if($projectId === 'console') {
if ($projectId === 'console') {
return $console;
}
@@ -754,7 +838,7 @@ App::setResource('project', function($dbForConsole, $request, $console) {
return $project;
}, ['dbForConsole', 'request', 'console']);
App::setResource('console', function() {
App::setResource('console', function () {
return new Document([
'$id' => 'console',
'name' => 'Appwrite',
@@ -786,7 +870,7 @@ App::setResource('console', function() {
]);
}, []);
App::setResource('dbForProject', function($db, $cache, $project) {
App::setResource('dbForProject', function ($db, $cache, $project) {
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
@@ -796,7 +880,7 @@ App::setResource('dbForProject', function($db, $cache, $project) {
return $database;
}, ['db', 'cache', 'project']);
App::setResource('dbForConsole', function($db, $cache) {
App::setResource('dbForConsole', function ($db, $cache) {
$cache = new Cache(new RedisCache($cache));
$database = new Database(new MariaDB($db), $cache);
@@ -807,25 +891,27 @@ App::setResource('dbForConsole', function($db, $cache) {
}, ['db', 'cache']);
App::setResource('deviceLocal', function() {
App::setResource('deviceLocal', function () {
return new Local();
});
App::setResource('deviceFiles', function($project) {
App::setResource('deviceFiles', function ($project) {
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceFunctions', function($project) {
App::setResource('deviceFunctions', function ($project) {
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
}, ['project']);
App::setResource('deviceBuilds', function($project) {
App::setResource('deviceBuilds', function ($project) {
return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
}, ['project']);
function getDevice($root): Device {
function getDevice($root): Device
{
switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) {
case Storage::DEVICE_LOCAL:default:
case Storage::DEVICE_LOCAL:
default:
return new Local($root);
case Storage::DEVICE_S3:
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
@@ -841,10 +927,31 @@ function getDevice($root): Device {
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
$doSpacesAcl = 'private';
return new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = App::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
$backblazeSecretKey = App::getEnv('_APP_STORAGE_BACKBLAZE_SECRET', '');
$backblazeRegion = App::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = App::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
case Storage::DEVICE_LINODE:
$linodeAccessKey = App::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
$linodeSecretKey = App::getEnv('_APP_STORAGE_LINODE_SECRET', '');
$linodeRegion = App::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = App::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
case Storage::DEVICE_WASABI:
$wasabiAccessKey = App::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
$wasabiSecretKey = App::getEnv('_APP_STORAGE_WASABI_SECRET', '');
$wasabiRegion = App::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = App::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
}
}
App::setResource('mode', function($request) {
App::setResource('mode', function ($request) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -855,7 +962,7 @@ App::setResource('mode', function($request) {
return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
}, ['request']);
App::setResource('geodb', function($register) {
App::setResource('geodb', function ($register) {
/** @var Utopia\Registry\Registry $register */
return $register->get('geodb');
}, ['register']);
+10 -8
View File
@@ -2,27 +2,28 @@
/**
* Init
*
*
* Inializes both Appwrite API entry point, queue workers, and CLI tasks.
* Set configuration, framework resources, app constants
*
*
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (file_exists(__DIR__.'/../vendor/autoload.php')) {
require __DIR__.'/../vendor/autoload.php';
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
}
use Utopia\Preloader\Preloader;
include __DIR__.'/controllers/general.php';
include __DIR__ . '/controllers/general.php';
$preloader = new Preloader();
foreach ([
foreach (
[
realpath(__DIR__ . '/../vendor/composer'),
realpath(__DIR__ . '/../vendor/amphp'),
realpath(__DIR__ . '/../vendor/felixfbecker'),
@@ -34,8 +35,9 @@ foreach ([
realpath(__DIR__ . '/../vendor/symfony'),
realpath(__DIR__ . '/../vendor/mongodb'),
realpath(__DIR__ . '/../vendor/utopia-php/websocket'), // TODO: remove workerman autoload
] as $key => $value) {
if($value !== false) {
] as $key => $value
) {
if ($value !== false) {
$preloader->ignore($value);
}
}
+29 -57
View File
@@ -55,10 +55,10 @@ $adapter
$server = new Server($adapter);
$logError = function(Throwable $error, string $action) use ($register) {
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('logger');
if($logger) {
if ($logger) {
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
@@ -82,7 +82,7 @@ $logError = function(Throwable $error, string $action) use ($register) {
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$responseCode = $logger->addLog($log);
Console::info('Realtime log pushed with status code: '.$responseCode);
Console::info('Realtime log pushed with status code: ' . $responseCode);
}
Console::error('[Error] Type: ' . get_class($error));
@@ -113,10 +113,10 @@ function getDatabase(Registry &$register, string $namespace)
throw new Exception('Collection not ready');
}
break; // leave loop if successful
} catch(\Exception $e) {
} 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());
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
@@ -129,8 +129,7 @@ function getDatabase(Registry &$register, string $namespace)
$register->get('redisPool')->put($redis);
}
];
};
}
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
sleep(5); // wait for the initial database schema to be ready
@@ -151,7 +150,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
'timestamp' => time(),
'value' => '{}'
]);
$statsDocument = Authorization::skip(fn() => $database->createDocument('realtime', $document));
$statsDocument = Authorization::skip(fn () => $database->createDocument('realtime', $document));
} catch (\Throwable $th) {
call_user_func($logError, $th, "createWorkerDocument");
} finally {
@@ -163,28 +162,6 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
* Save current connections to the Database every 5 seconds.
*/
Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) {
/** @var Document $statsDocument */
foreach ($stats as $projectId => $value) {
$connections = $stats->get($projectId, 'connections') ?? 0;
$messages = $stats->get($projectId, 'messages' ?? 0);
$usage = new Event('v1-usage', 'UsageV1');
$usage
->setParam('projectId', $projectId)
->setParam('realtimeConnections', $connections)
->setParam('realtimeMessages', $messages)
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0);
$stats->set($projectId, [
'messages' => 0,
'connections' => 0
]);
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
$usage->trigger();
}
}
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -200,7 +177,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
->setAttribute('timestamp', time())
->setAttribute('value', json_encode($payload));
Authorization::skip(fn() => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (\Throwable $th) {
call_user_func($logError, $th, "updateWorkerDocument");
} finally {
@@ -210,7 +187,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
});
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started succefully');
Console::success('Worker ' . $workerId . ' started successfully');
$attempts = 0;
$start = time();
@@ -220,14 +197,13 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
* Sending current connections to project channels on the console project every 5 seconds.
*/
if ($realtime->hasSubscriber('console', 'role:member', 'project')) {
[$database, $returnDatabase] = getDatabase($register, '_console');
$payload = [];
$list = Authorization::skip(fn() => $database->find('realtime', [
new Query('timestamp', Query::TYPE_GREATER, [(time() - 15)])
]));
$list = Authorization::skip(fn () => $database->find('realtime', [
new Query('timestamp', Query::TYPE_GREATER, [(time() - 15)])
]));
/**
* Aggregate stats across containers.
@@ -251,7 +227,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
'project' => 'console',
'roles' => ['team:' . $stats->get($projectId, 'teamId')],
'data' => [
'event' => 'stats.connections',
'events' => ['stats.connections'],
'channels' => ['project'],
'timestamp' => time(),
'payload' => [
@@ -278,7 +254,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
'project' => 'console',
'roles' => ['role:guest'],
'data' => [
'event' => 'test.event',
'events' => ['test.event'],
'channels' => ['tests'],
'timestamp' => time(),
'payload' => $payload
@@ -321,19 +297,16 @@ $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]));
} else {
return;
[$database, $returnDatabase] = getDatabase($register, "_{$projectId}");
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']);
call_user_func($returnDatabase);
}
[$database, $returnDatabase] = getDatabase($register, "_{$projectId}");
$user = $database->getDocument('users', $userId);
$roles = Auth::getRoles($user);
$realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']);
call_user_func($returnDatabase);
}
$receivers = $realtime->getSubscribers($event);
@@ -362,10 +335,9 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Pub/sub error: ' . $th->getMessage());
$register->get('redisPool')->put($redis);
$attempts++;
sleep(DATABASE_RECONNECT_SLEEP);
continue;
}
$attempts++;
}
Console::error('Failed to restart pub/sub...');
@@ -383,10 +355,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::info("Connection open (user: {$connection})");
App::setResource('db', fn() => $db);
App::setResource('cache', fn() => $redis);
App::setResource('request', fn() => $request);
App::setResource('response', fn() => $response);
App::setResource('db', fn () => $db);
App::setResource('cache', fn () => $redis);
App::setResource('request', fn () => $request);
App::setResource('response', fn () => $response);
try {
/** @var \Utopia\Database\Document $user */
@@ -534,7 +506,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
switch ($message['type']) {
/**
/**
* This type is used to authenticate.
*/
case 'authentication':
+55 -60
View File
@@ -19,46 +19,41 @@ $cli
/ \ ) __/ ) __/\ /\ / ) / )( )( ) _) _ )(( O )
\_/\_/(__) (__) (_/\_)(__\_)(__) (__) (____)(_)(__)\__/ ");
Console::log("\n".'👩‍⚕️ Running '.APP_NAME.' Doctor for version '.App::getEnv('_APP_VERSION', 'UNKNOWN').' ...'."\n");
Console::log("\n" . '👩‍⚕️ Running ' . APP_NAME . ' Doctor for version ' . App::getEnv('_APP_VERSION', 'UNKNOWN') . ' ...' . "\n");
Console::log('Checking for production best practices...');
$domain = new Domain(App::getEnv('_APP_DOMAIN'));
if(!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 Hostname has no public suffix ('.$domain->get().')');
}
else {
Console::log('🟢 Hostname has a public suffix ('.$domain->get().')');
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 Hostname has no public suffix (' . $domain->get() . ')');
} else {
Console::log('🟢 Hostname has a public suffix (' . $domain->get() . ')');
}
$domain = new Domain(App::getEnv('_APP_DOMAIN_TARGET'));
if(!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 CNAME target has no public suffix ('.$domain->get().')');
}
else {
Console::log('🟢 CNAME target has a public suffix ('.$domain->get().')');
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 CNAME target has no public suffix (' . $domain->get() . ')');
} else {
Console::log('🟢 CNAME target has a public suffix (' . $domain->get() . ')');
}
if(App::getEnv('_APP_OPENSSL_KEY_V1') === 'your-secret-key' || empty(App::getEnv('_APP_OPENSSL_KEY_V1'))) {
if (App::getEnv('_APP_OPENSSL_KEY_V1') === 'your-secret-key' || empty(App::getEnv('_APP_OPENSSL_KEY_V1'))) {
Console::log('🔴 Not using a unique secret key for encryption');
}
else {
} else {
Console::log('🟢 Using a unique secret key for encryption');
}
if(App::getEnv('_APP_ENV', 'development') !== 'production') {
if (App::getEnv('_APP_ENV', 'development') !== 'production') {
Console::log('🔴 App environment is set for development');
}
else {
} else {
Console::log('🟢 App environment is set for production');
}
if('enabled' !== App::getEnv('_APP_OPTIONS_ABUSE', 'disabled')) {
if ('enabled' !== App::getEnv('_APP_OPTIONS_ABUSE', 'disabled')) {
Console::log('🔴 Abuse protection is disabled');
}
else {
} else {
Console::log('🟢 Abuse protection is enabled');
}
@@ -66,20 +61,19 @@ $cli
$authWhitelistEmails = App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null);
$authWhitelistIPs = App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null);
if(empty($authWhitelistRoot)
if (
empty($authWhitelistRoot)
&& empty($authWhitelistEmails)
&& empty($authWhitelistIPs)
) {
Console::log('🔴 Console access limits are disabled');
}
else {
} else {
Console::log('🟢 Console access limits are enabled');
}
if('enabled' !== App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled')) {
if ('enabled' !== App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled')) {
Console::log('🔴 HTTPS force option is disabled');
}
else {
} else {
Console::log('🟢 HTTPS force option is enabled');
}
@@ -87,7 +81,7 @@ $cli
$providerName = App::getEnv('_APP_LOGGING_PROVIDER', '');
$providerConfig = App::getEnv('_APP_LOGGING_CONFIG', '');
if(empty($providerName) || empty($providerConfig) || !Logger::hasProvider($providerName)) {
if (empty($providerName) || empty($providerConfig) || !Logger::hasProvider($providerName)) {
Console::log('🔴 Logging adapter is disabled');
} else {
Console::log('🟢 Logging adapter is enabled (' . $providerName . ')');
@@ -96,7 +90,7 @@ $cli
\sleep(0.2);
try {
Console::log("\n".'Checking connectivity...');
Console::log("\n" . 'Checking connectivity...');
} catch (\Throwable $th) {
//throw $th;
}
@@ -122,15 +116,16 @@ $cli
Console::error('Cache............disconnected 👎');
}
if(App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled
try {
$antivirus = new Network(App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310));
$antivirus = new Network(
App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
if((@$antivirus->ping())) {
if ((@$antivirus->ping())) {
Console::success('Antivirus...........connected 👍');
}
else {
} else {
Console::error('Antivirus........disconnected 👎');
}
} catch (\Throwable $th) {
@@ -155,7 +150,7 @@ $cli
$host = App::getEnv('_APP_STATSD_HOST', 'telegraf');
$port = App::getEnv('_APP_STATSD_PORT', 8125);
if($fp = @\fsockopen('udp://'.$host, $port, $errCode, $errStr, 2)){
if ($fp = @\fsockopen('udp://' . $host, $port, $errCode, $errStr, 2)) {
Console::success('StatsD..............connected 👍');
\fclose($fp);
} else {
@@ -165,7 +160,7 @@ $cli
$host = App::getEnv('_APP_INFLUXDB_HOST', '');
$port = App::getEnv('_APP_INFLUXDB_PORT', '');
if($fp = @\fsockopen($host, $port, $errCode, $errStr, 2)){
if ($fp = @\fsockopen($host, $port, $errCode, $errStr, 2)) {
Console::success('InfluxDB............connected 👍');
\fclose($fp);
} else {
@@ -177,26 +172,26 @@ $cli
Console::log('');
Console::log('Checking volumes...');
foreach ([
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES
] as $key => $volume) {
] as $key => $volume
) {
$device = new Local($volume);
if (\is_readable($device->getRoot())) {
Console::success('🟢 '.$key.' Volume is readable');
}
else {
Console::error('🔴 '.$key.' Volume is unreadable');
Console::success('🟢 ' . $key . ' Volume is readable');
} else {
Console::error('🔴 ' . $key . ' Volume is unreadable');
}
if (\is_writable($device->getRoot())) {
Console::success('🟢 '.$key.' Volume is writeable');
}
else {
Console::error('🔴 '.$key.' Volume is unwriteable');
Console::success('🟢 ' . $key . ' Volume is writeable');
} else {
Console::error('🔴 ' . $key . ' Volume is unwriteable');
}
}
@@ -205,44 +200,44 @@ $cli
Console::log('');
Console::log('Checking disk space usage...');
foreach ([
foreach (
[
'Uploads' => APP_STORAGE_UPLOADS,
'Cache' => APP_STORAGE_CACHE,
'Config' => APP_STORAGE_CONFIG,
'Certs' => APP_STORAGE_CERTIFICATES
] as $key => $volume) {
] as $key => $volume
) {
$device = new Local($volume);
$percentage = (($device->getPartitionTotalSpace() - $device->getPartitionFreeSpace())
/ $device->getPartitionTotalSpace()) * 100;
$message = $key.' Volume has '.Storage::human($device->getPartitionFreeSpace()) . ' free space ('.\round($percentage, 2).'% used)';
$message = $key . ' Volume has ' . Storage::human($device->getPartitionFreeSpace()) . ' free space (' . \round($percentage, 2) . '% used)';
if ($percentage < 80) {
Console::success('🟢 ' . $message);
}
else {
} else {
Console::error('🔴 ' . $message);
}
}
try {
if(App::isProduction()) {
if (App::isProduction()) {
Console::log('');
$version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost').'/v1/health/version'), true);
$version = \json_decode(@\file_get_contents(App::getEnv('_APP_HOME', 'http://localhost') . '/v1/health/version'), true);
if ($version && isset($version['version'])) {
if(\version_compare($version['version'], App::getEnv('_APP_VERSION', 'UNKNOWN')) === 0) {
Console::info('You are running the latest version of '.APP_NAME.'! 🥳');
}
else {
Console::info('A new version ('.$version['version'].') is available! 🥳'."\n");
if (\version_compare($version['version'], App::getEnv('_APP_VERSION', 'UNKNOWN')) === 0) {
Console::info('You are running the latest version of ' . APP_NAME . '! 🥳');
} else {
Console::info('A new version (' . $version['version'] . ') is available! 🥳' . "\n");
}
} else {
Console::error('Failed to check for a newer version'."\n");
Console::error('Failed to check for a newer version' . "\n");
}
}
} catch (\Throwable $th) {
Console::error('Failed to check for a newer version'."\n");
Console::error('Failed to check for a newer version' . "\n");
}
});
+50 -44
View File
@@ -18,7 +18,7 @@ $cli
->param('httpsPort', '', new Text(4), 'Server HTTPS port', true)
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
->param('interactive','Y', new Text(1), 'Run an interactive session', true)
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->action(function ($httpPort, $httpsPort, $organization, $image, $interactive) {
/**
* 1. Start - DONE
@@ -31,7 +31,7 @@ $cli
* 2.4 Ask for all required vars not given as CLI args and if in interactive mode
* Otherwise, just use default vars. - DONE
* 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
* 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
* 6. Run data migration
@@ -48,8 +48,8 @@ $cli
*/
$analytics = new GoogleAnalytics('UA-26264668-9', uniqid('server.', true));
foreach($config as $category) {
foreach($category['variables'] ?? [] as $var) {
foreach ($config as $category) {
foreach ($category['variables'] ?? [] as $var) {
$vars[] = $var;
}
}
@@ -59,17 +59,17 @@ $cli
// Create directory with write permissions
if (null !== $path && !\file_exists(\dirname($path))) {
if (!@\mkdir(\dirname($path), 0755, true)) {
Console::error('Can\'t create directory '.\dirname($path));
Console::error('Can\'t create directory ' . \dirname($path));
Console::exit(1);
}
}
$data = @file_get_contents($path.'/docker-compose.yml');
$data = @file_get_contents($path . '/docker-compose.yml');
if($data !== false) {
if ($data !== false) {
$time = \time();
Console::info('Compose file found, creating backup: docker-compose.yml.'.$time.'.backup');
file_put_contents($path.'/docker-compose.yml.'.$time.'.backup',$data);
Console::info('Compose file found, creating backup: docker-compose.yml.' . $time . '.backup');
file_put_contents($path . '/docker-compose.yml.' . $time . '.backup', $data);
$compose = new Compose($data);
$appwrite = $compose->getService('appwrite');
$oldVersion = ($appwrite) ? $appwrite->getImageVersion() : null;
@@ -83,33 +83,39 @@ $cli
Console::warning('Traefik not found. Falling back to default ports.');
}
if($oldVersion) {
foreach($compose->getServices() as $service) { // Fetch all env vars from previous compose file
if(!$service) {
if ($oldVersion) {
foreach ($compose->getServices() as $service) { // Fetch all env vars from previous compose file
if (!$service) {
continue;
}
$env = $service->getEnvironment()->list();
foreach ($env as $key => $value) {
foreach($vars as &$var) {
if($var['name'] === $key) {
if (is_null($value)) {
continue;
}
foreach ($vars as &$var) {
if ($var['name'] === $key) {
$var['default'] = $value;
}
}
}
}
$data = @file_get_contents($path.'/.env');
$data = @file_get_contents($path . '/.env');
if($data !== false) { // Fetch all env vars from previous .env file
Console::info('Env file found, creating backup: .env.'.$time.'.backup');
file_put_contents($path.'/.env.'.$time.'.backup',$data);
if ($data !== false) { // Fetch all env vars from previous .env file
Console::info('Env file found, creating backup: .env.' . $time . '.backup');
file_put_contents($path . '/.env.' . $time . '.backup', $data);
$env = new Env($data);
foreach ($env->list() as $key => $value) {
foreach($vars as &$var) {
if($var['name'] === $key) {
if (is_null($value)) {
continue;
}
foreach ($vars as &$var) {
if ($var['name'] === $key) {
$var['default'] = $value;
}
}
@@ -117,60 +123,60 @@ $cli
}
foreach ($ports as $key => $value) {
if($value === $defaultHTTPPort) {
if ($value === $defaultHTTPPort) {
$defaultHTTPPort = $key;
}
if($value === $defaultHTTPSPort) {
if ($value === $defaultHTTPSPort) {
$defaultHTTPSPort = $key;
}
}
}
}
if(empty($httpPort)) {
$httpPort = Console::confirm('Choose your server HTTP port: (default: '.$defaultHTTPPort.')');
if (empty($httpPort)) {
$httpPort = Console::confirm('Choose your server HTTP port: (default: ' . $defaultHTTPPort . ')');
$httpPort = ($httpPort) ? $httpPort : $defaultHTTPPort;
}
if(empty($httpsPort)) {
$httpsPort = Console::confirm('Choose your server HTTPS port: (default: '.$defaultHTTPSPort.')');
if (empty($httpsPort)) {
$httpsPort = Console::confirm('Choose your server HTTPS port: (default: ' . $defaultHTTPSPort . ')');
$httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPSPort;
}
$input = [];
foreach($vars as $key => $var) {
if(!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) {
if($data && $var['default'] !== null) {
foreach ($vars as $key => $var) {
if (!empty($var['filter']) && ($interactive !== 'Y' || !Console::isInteractive())) {
if ($data && $var['default'] !== null) {
$input[$var['name']] = $var['default'];
continue;
}
if($var['filter'] === 'token') {
if ($var['filter'] === 'token') {
$input[$var['name']] = Auth::tokenGenerator();
continue;
}
if($var['filter'] === 'password') {
if ($var['filter'] === 'password') {
$input[$var['name']] = Auth::passwordGenerator();
continue;
}
}
if(!$var['required'] || !Console::isInteractive() || $interactive !== 'Y') {
if (!$var['required'] || !Console::isInteractive() || $interactive !== 'Y') {
$input[$var['name']] = $var['default'];
continue;
}
$input[$var['name']] = Console::confirm($var['question'].' (default: \''.$var['default'].'\')');
$input[$var['name']] = Console::confirm($var['question'] . ' (default: \'' . $var['default'] . '\')');
if(empty($input[$var['name']])) {
if (empty($input[$var['name']])) {
$input[$var['name']] = $var['default'];
}
}
$templateForCompose = new View(__DIR__.'/../views/install/compose.phtml');
$templateForEnv = new View(__DIR__.'/../views/install/env.phtml');
$templateForCompose = new View(__DIR__ . '/../views/install/compose.phtml');
$templateForEnv = new View(__DIR__ . '/../views/install/env.phtml');
$templateForCompose
->setParam('httpPort', $httpPort)
@@ -184,16 +190,16 @@ $cli
->setParam('vars', $input)
;
if(!file_put_contents($path.'/docker-compose.yml', $templateForCompose->render(false))) {
if (!file_put_contents($path . '/docker-compose.yml', $templateForCompose->render(false))) {
$message = 'Failed to save Docker Compose file';
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE.' - '.$message);
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE . ' - ' . $message);
Console::error($message);
Console::exit(1);
}
if(!file_put_contents($path.'/.env', $templateForEnv->render(false))) {
if (!file_put_contents($path . '/.env', $templateForEnv->render(false))) {
$message = 'Failed to save environment variables file';
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE.' - '.$message);
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE . ' - ' . $message);
Console::error($message);
Console::exit(1);
}
@@ -203,8 +209,8 @@ $cli
$stderr = '';
foreach ($input as $key => $value) {
if($value) {
$env .= $key.'='.\escapeshellarg($value).' ';
if ($value) {
$env .= $key . '=' . \escapeshellarg($value) . ' ';
}
}
@@ -214,13 +220,13 @@ $cli
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE.' - '.$message);
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE . ' - ' . $message);
Console::error($message);
Console::error($stderr);
Console::exit($exit);
} else {
$message = 'Appwrite installed successfully';
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE.' - '.$message);
$analytics->createEvent('install/server', 'install', APP_VERSION_STABLE . ' - ' . $message);
Console::success($message);
}
});
+98 -31
View File
@@ -1,57 +1,121 @@
<?php
global $cli;
global $register;
use Appwrite\Event\Event;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Database\Document;
use Utopia\Database\Query;
function getConsoleDB(): Database
{
global $register;
$attempts = 0;
do {
try {
$attempts++;
$cache = new Cache(new RedisCache($register->get('cache')));
$database = new Database(new MariaDB($register->get('db')), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace('_console'); // Main DB
if (!$database->exists($database->getDefaultDatabase(), 'certificates')) {
throw new \Exception('Console project 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;
}
$cli
->task('maintenance')
->desc('Schedules maintenance tasks and publishes them to resque')
->action(function () {
Console::title('Maintenance V1');
Console::success(APP_NAME.' maintenance process v1 has started');
Console::success(APP_NAME . ' maintenance process v1 has started');
function notifyDeleteExecutionLogs(int $interval)
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_EXECUTIONS,
'timestamp' => time() - $interval
]);
(new Delete())
->setType(DELETE_TYPE_EXECUTIONS)
->setTimestamp(time() - $interval)
->trigger();
}
function notifyDeleteAbuseLogs(int $interval)
function notifyDeleteAbuseLogs(int $interval)
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_ABUSE,
'timestamp' => time() - $interval
]);
(new Delete())
->setType(DELETE_TYPE_ABUSE)
->setTimestamp(time() - $interval)
->trigger();
}
function notifyDeleteAuditLogs(int $interval)
function notifyDeleteAuditLogs(int $interval)
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_AUDIT,
'timestamp' => time() - $interval
]);
(new Delete())
->setType(DELETE_TYPE_AUDIT)
->setTimestamp(time() - $interval)
->trigger();
}
function notifyDeleteUsageStats(int $interval30m, int $interval1d)
function notifyDeleteUsageStats(int $interval30m, int $interval1d)
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_USAGE,
'timestamp1d' => time() - $interval1d,
'timestamp30m' => time() - $interval30m,
]);
(new Delete())
->setType(DELETE_TYPE_USAGE)
->setTimestamp1d(time() - $interval1d)
->setTimestamp30m(time() - $interval30m)
->trigger();
}
function notifyDeleteConnections()
function notifyDeleteConnections()
{
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
'type' => DELETE_TYPE_REALTIME,
'timestamp' => time() - 60
]);
(new Delete())
->setType(DELETE_TYPE_REALTIME)
->setTimestamp(time() - 60)
->trigger();
}
function renewCertificates($dbForConsole)
{
$time = date('d-m-Y H:i:s', time());
$certificates = $dbForConsole->find('certificates', [
new Query('attempts', Query::TYPE_LESSEREQUAL, [5]), // Maximum 5 attempts
new Query('renewDate', Query::TYPE_LESSEREQUAL, [\time()]) // includes 60 days cooldown (we have 30 days to renew)
], 200); // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains)
if (\count($certificates) > 0) {
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
$event = new Certificate();
foreach ($certificates as $certificate) {
$event
->setDomain(new Document([
'domain' => $certificate->getAttribute('domain')
]))
->trigger();
}
} else {
Console::info("[{$time}] No certificates for renewal.");
}
}
// # of days in seconds (1 day = 86400s)
@@ -59,16 +123,19 @@ $cli
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
$auditLogRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', '1209600');
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600');//36 hours
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours
$usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days
Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
$database = getConsoleDB();
$time = date('d-m-Y H:i:s', time());
Console::info("[{$time}] Notifying deletes workers every {$interval} seconds");
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
notifyDeleteExecutionLogs($executionLogsRetention);
notifyDeleteAbuseLogs($abuseLogsRetention);
notifyDeleteAuditLogs($auditLogRetention);
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
notifyDeleteConnections();
renewCertificates($database);
}, $interval);
});
});
+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', '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', '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.13.0',
'X-Appwrite-Response-Format' => '0.14.0',
]);
try {
+1 -2
View File
@@ -25,7 +25,7 @@ $cli
$response = new Response(new HttpResponse());
$mocks = ($mode === 'mocks');
App::setResource('request', fn () => new Request);
App::setResource('request', fn () => new Request());
App::setResource('response', fn () => $response);
App::setResource('db', fn () => $db);
App::setResource('cache', fn () => $redis);
@@ -125,7 +125,6 @@ $cli
foreach (['swagger2', 'open-api3'] as $format) {
foreach ($platforms as $platform) {
$routes = [];
$models = [];
$services = [];
+13 -12
View File
@@ -2,22 +2,23 @@
global $cli;
use Appwrite\Event\Certificate;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Validator\Hostname;
$cli
->task('ssl')
->desc('Validate server certificates')
->action(function () {
$domain = App::getEnv('_APP_DOMAIN', '');
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->action(function ($domain) {
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
Console::log('Issue a TLS certificate for master domain ('.$domain.') in 30 seconds.
Make sure your domain points to your server or restart to try again.');
ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [
'document' => [],
'domain' => $domain,
'validateTarget' => false,
'validateCNAME' => false,
]);
});
(new Certificate())
->setDomain(new Document([
'domain' => $domain
]))
->setSkipRenewCheck(true)
->trigger();
});
+76 -22
View File
@@ -36,7 +36,7 @@ use Utopia\Database\Validator\Authorization;
* database.collections.{collectionId}.documents.delete
*
* Storage
*
*
* storage.buckets.create
* storage.buckets.read
* storage.buckets.update
@@ -285,7 +285,7 @@ $cli
try {
$attempts++;
$database = $client->selectDB('telegraf');
if(in_array('telegraf', $client->listDatabases())) {
if (in_array('telegraf', $client->listDatabases())) {
break; // leave the do-while if successful
}
} catch (\Throwable $th) {
@@ -386,7 +386,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;
@@ -418,27 +418,26 @@ $cli
// Get total storage
$dbForProject->setNamespace('_' . $projectId);
$storageTotal = $dbForProject->sum('deployments', 'size');
$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' => $storageTotal,
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $storageTotal)
$document->setAttribute('value', $deploymentsTotal)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@@ -450,17 +449,17 @@ $cli
'period' => '1d',
'time' => $time,
'metric' => 'storage.deployments.total',
'value' => $storageTotal,
'value' => $deploymentsTotal,
'type' => 1,
]));
} else {
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $storageTotal)
$document->setAttribute('value', $deploymentsTotal)
);
}
} catch(\Exception $e) {
} catch (\Exception $e) {
Console::warning("Failed to save data for project {$projectId} and metric storage.deployments.total: {$e->getMessage()}");
Console::warning($e->getTraceAsString());
}
@@ -614,7 +613,7 @@ $cli
// check if sum calculation is required
$total = $subOptions['total'] ?? [];
if(empty($total)) {
if (empty($total)) {
continue;
}
@@ -639,8 +638,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $total));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@@ -656,10 +658,12 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $total));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $total)
);
}
}
}
} while (!empty($parents));
@@ -714,7 +718,7 @@ $cli
}
/**
* Inserting project level sums for sub collections like storage.total
* Inserting project level sums for sub collections like storage.files.total
*/
foreach ($subCollectionTotals as $subCollection => $count) {
$dbForProject->setNamespace("_{$projectId}");
@@ -734,8 +738,11 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$dbForProject->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $count)
);
}
$time = (int) (floor(time() / 86400) * 86400); // Time rounded to nearest day
@@ -751,8 +758,55 @@ $cli
'type' => 1,
]));
} else {
$dbForProject->updateDocument('stats', $document->getId(),
$document->setAttribute('value', $count));
$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) {
@@ -769,4 +823,4 @@ $cli
Console::info("[{$now}] Aggregation took {$loopTook} seconds");
}, $interval);
});
});
+4 -4
View File
@@ -13,13 +13,13 @@ $cli
$config = Config::getParam('variables', []);
$vars = [];
foreach($config as $category) {
foreach($category['variables'] ?? [] as $var) {
foreach ($config as $category) {
foreach ($category['variables'] ?? [] as $var) {
$vars[] = $var;
}
}
foreach ($vars as $key => $value) {
Console::log('- '.$value['name'].'='.App::getEnv($value['name'], ''));
Console::log('- ' . $value['name'] . '=' . App::getEnv($value['name'], ''));
}
});
});
+11 -11
View File
@@ -621,7 +621,7 @@ $logs = $this->getParam('logs', null);
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
x-data="{ array: false, required: false, size: null }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
@@ -631,7 +631,7 @@ $logs = $this->getParam('logs', null);
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Allowed Characters A-Z, a-z, 0-9, and non-leading underscore, hyphen and dot</div>
<label for="string-length">Size</label>
<input id="string-length" name="size" type="number" class="margin-bottom" autocomplete="off" required value="255" data-cast-to="integer" />
<input id="string-length" name="size" type="number" class="margin-bottom" autocomplete="off" required value="255" data-cast-to="integer" x-model="size" />
<div class="margin-bottom">
<input x-model="required" name="required" class="button switch" type="checkbox" /> &nbsp; Required <span class="tooltip" data-tooltip="Mark whether this is a required attribute"><i class="icon-info-circled"></i></span>
@@ -643,7 +643,7 @@ $logs = $this->getParam('logs', null);
<label for="xdefault">Default Value</label>
<template x-if="!(array || required)">
<input name="xdefault" type="text" class="margin-bottom-large" autocomplete="off">
<input name="xdefault" type="text" class="margin-bottom-large" autocomplete="off" :maxlength="size">
</template>
<template x-if="(array || required)">
<input name="xdefault" type="text" class="margin-bottom-large" autocomplete="off" disabled value="">
@@ -677,7 +677,7 @@ $logs = $this->getParam('logs', null);
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
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="collectionId" data-ls-bind="{{router.params.id}}" />
@@ -697,18 +697,18 @@ $logs = $this->getParam('logs', null);
<div class="row responsive thin">
<div class="col span-6 margin-bottom-small">
<label for="integer-min">Min</label>
<input id="integer-min" type="number" step="1" class="full-width" name="min" autocomplete="off" data-cast-to="integer" />
<input id="integer-min" type="number" step="1" class="full-width" name="min" autocomplete="off" data-cast-to="integer" x-model="min" />
</div>
<div class="col span-6 margin-bottom-small">
<label for="integer-max">Max</label>
<input id="integer-max" type="number" step="1" class="full-width" name="max" autocomplete="off" data-cast-to="integer" />
<input id="integer-max" type="number" step="1" class="full-width" name="max" autocomplete="off" data-cast-to="integer" x-model="max" />
</div>
</div>
<label for="integer-default">Default Value</label>
<template x-if="!(array || required)">
<input name="xdefault" type="number" class="margin-bottom-large" autocomplete="off" data-cast-to="integer">
<input name="xdefault" type="number" class="margin-bottom-large" autocomplete="off" data-cast-to="integer" :min="min" :max="max">
</template>
<template x-if="(array || required)">
<input name="xdefault" type="number" class="margin-bottom-large" autocomplete="off" data-cast-to="integer" disabled>
@@ -742,7 +742,7 @@ $logs = $this->getParam('logs', null);
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
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="collectionId" data-ls-bind="{{router.params.id}}" />
@@ -762,18 +762,18 @@ $logs = $this->getParam('logs', null);
<div class="row responsive thin">
<div class="col span-6 margin-bottom-small">
<label for="float-min">Min</label>
<input id="float-min" type="number" class="full-width" name="min" step="any" autocomplete="off" data-cast-to="float" />
<input id="float-min" type="number" class="full-width" name="min" step="any" autocomplete="off" data-cast-to="float" x-model="min" />
</div>
<div class="col span-6 margin-bottom-small">
<label for="float-max">Max</label>
<input id="float-max" type="number" class="full-width" name="max" step="any" autocomplete="off" data-cast-to="float" />
<input id="float-max" type="number" class="full-width" name="max" step="any" autocomplete="off" data-cast-to="float" x-model="max" />
</div>
</div>
<label for="float-default">Default Value</label>
<template x-if="!(array || required)">
<input name="xdefault" type="number" step="any" class="margin-bottom-large" autocomplete="off" data-cast-to="float">
<input name="xdefault" type="number" step="any" class="margin-bottom-large" autocomplete="off" data-cast-to="float" :min="min" :max="max">
</template>
<template x-if="(array || required)">
<input name="xdefault" type="number" step="any" class="margin-bottom-large" autocomplete="off" data-cast-to="float" disabled>
+25 -6
View File
@@ -55,17 +55,26 @@ $logs = $this->getParam('logs', null);
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Database Document"
data-service="{{|documentAction}}"
data-name="project-document"
data-scope="sdk"
data-event="submit"
data-success="trigger,redirect"
data-success-param-trigger-events="database.updateDocument"
data-success-param-redirect-url="/console/database/document?id={{serviceData.$id}}&collection={{project-collection.$id}}&project={{router.params.project}}"
data-failure="alert"
data-failure-param-alert-text="Failed to update document"
data-failure-param-alert-classname="error">
data-failure-param-alert-classname="error"
<?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-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-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}}" />
<?php if(!$new): ?><input type="hidden" name="documentId" data-ls-bind="{{project-document.$id}}" /><?php endif; ?>
@@ -104,6 +113,8 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:min="attr.min"
:max="attr.max"
x-model="doc[attr.key]"
data-cast-to="integer" />
</template>
@@ -114,6 +125,8 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:min="attr.min"
:max="attr.max"
x-model="doc[attr.key]"
data-cast-to="float" />
</template>
@@ -131,6 +144,7 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:maxlength="attr.size"
x-model="doc[attr.key]"
data-cast-to="string"></textarea>
</template>
@@ -197,6 +211,8 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:min="attr.min"
:max="attr.max"
x-model="doc[attr.key][index]"
data-cast-to="integer" />
</template>
@@ -207,6 +223,8 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:min="attr.min"
:max="attr.max"
x-model="doc[attr.key][index]"
data-cast-to="float" />
</template>
@@ -226,6 +244,7 @@ $logs = $this->getParam('logs', null);
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
:maxlength="attr.size"
x-model="doc[attr.key][index]"
data-cast-to="string"></textarea>
</template>
+84 -28
View File
@@ -1,9 +1,33 @@
<?php
$fileLimit = $this->getParam('fileLimit', 0);
$fileLimitHuman = $this->getParam('fileLimitHuman', 0);
$events = array_keys($this->getParam('events', []));
$events = $this->getParam('events', []);
$timeout = $this->getParam('timeout', 900);
$usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
$patterns = [];
foreach ($events as $name => $event) {
$patterns[] = $name;
foreach ($event as $key => $value) {
if (!\str_starts_with($key, '$')) {
if (!($value['$resource'] ?? false)) {
$patterns[] = "{$name}.{$key}";
} else {
$patterns[] = $key;
foreach ($value as $key2 => $value2) {
if (!\str_starts_with($key2, '$')) {
if (!($value2['$resource'] ?? false)) {
$patterns[] = "{$key}.{$key2}";
}
}
}
}
}
}
}
sort($patterns);
?>
<div
data-service="functions.get"
@@ -396,12 +420,11 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<h1>STDOUT</h1>
<div class="margin-bottom ide" data-ls-if="({{execution.stdout.length}})">
<pre data-ls-bind="{{execution.stdout}}"></pre>
<!-- <input type="hidden" data-ls-bind="{{execution.stdout}}" data-forms-code="bash" /> -->
<div class="margin-bottom ide" data-ls-if="({{execution.response.length}})">
<pre data-ls-bind="{{execution.response}}"></pre>
</div>
<div class="margin-bottom" data-ls-if="(!{{execution.stdout.length}})">
<div class="margin-bottom" data-ls-if="(!{{execution.response.length}})">
<p>No output was logged.</p>
</div>
</div>
@@ -469,7 +492,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
</div>
</div>
</li>
<li data-state="/console/functions/function/settings?id={{router.params.id}}&project={{router.params.project}}">
<li data-state="/console/functions/function/settings?id={{router.params.id}}&project={{router.params.project}}" x-data="events">
<h2>Settings</h2>
<div class="row responsive margin-top-negative">
@@ -507,32 +530,30 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<div class="text-size-small text-fade margin-bottom margin-top-negative-small">Max value is <?php echo $this->escape(number_format($timeout)); ?> seconds (<?php echo $this->escape((int) ($timeout / 60)); ?> minutes)</div>
</section>
<section class="margin-bottom" data-forms-select-all>
<label for="events" class="margin-bottom">Events <span class="tooltip small" data-tooltip="Choose which events should trigger this function."><i class="icon-info-circled"></i></span></label>
<div class="row responsive thin margin-top-small">
<?php foreach ($events as $i => $event): ?>
<div class="col span-6 text-one-liner margin-bottom text-height-large text-size-small" title="<?php echo $event; ?>">
<input type="checkbox" name="events" data-ls-bind="{{project-function.events}}" id="<?php echo $event; ?>" value="<?php echo $event; ?>" data-by-key="true" />
&nbsp;
<label class="inline" for="<?php echo $event; ?>"><?php echo $event; ?></label>
<section class="margin-bottom-small" data-ls-attrs="x-init=load({{project-function.events}})">
<label class="margin-bottom-small">Events <span class="tooltip small" data-tooltip="Set events that will trigger your function."><i class="icon-info-circled"></i></span></label>
<div>
<template x-for="event in Array.from(events)">
<div class="row events responsive thin margin-bottom-small">
<div class="col span-12 margin-bottom-small">
<span class="text" x-text="event"></span>
<span class="action" @click="removeEvent(event)">
<i class="icon-trash"></i>
</span>
</div>
<input name="events" data-cast-to="array" type="hidden" :value="event"></input>
</div>
<?php if (($i + 1) % 2 === 0): ?>
</div>
<div class="row responsive thin">
<?php endif;?>
<?php endforeach;?>
</template>
</div>
<button class="margin-end margin-bottom-small reverse" type="button" @click="showModal($refs.modal_function)">Add Event</button>
</section>
<label for="schedule">Schedule (CRON Syntax) <span class="tooltip small" data-tooltip="Set a CRON schedule to trigger this function."><i class="icon-info-circled"></i></span></label>
<input type="text" id="function-schedule" class="full-width" name="schedule" autocomplete="off" data-ls-bind="{{project-function.schedule}}" placeholder="* * * * *" />
<div class="text-size-small text-fade margin-bottom margin-top-negative-small">Leave blank for no schedule</div>
<h3 class="margin-bottom-small">Variables <span class="tooltip small" data-tooltip="Set variables or secret keys that will be passed as env vars to your function at runtime."><i class="icon-info-circled"></i></span></h3>
<label class="margin-bottom-small">Variables <span class="tooltip small" data-tooltip="Set variables or secret keys that will be passed as env vars to your function at runtime."><i class="icon-info-circled"></i></span></label>
<div data-ls-if="(!{{project-function.vars.length}})">
<hr class="margin-bottom margin-top-no" />
<fieldset name="vars" data-cast-to="object">
<div data-ls-loop="project-function.vars" data-ls-as="var" id="project-vars" style="visibility: visible;">
<div class="margin-bottom-small">
@@ -541,7 +562,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<input type="hidden" data-forms-key-value data-ls-attrs="name={{$index}}" data-ls-bind="{{var}}" />
</div>
<div class="col span-2">
<button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-cancel"></i></button>
<button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-trash"></i></button>
</div>
</div>
</div>
@@ -554,14 +575,14 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<input type="hidden" data-ls-attrs="data-forms-key-value"/>
</div>
<div class="col span-2">
<button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-cancel"></i></button>
<button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-trash"></i></button>
</div>
</div>
</div>
</div>
</fieldset>
<hr class="margin-bottom margin-top-small" />
</div>
<hr class="margin-bottom margin-top-small" />
<button>Update</button>
</div>
@@ -601,6 +622,41 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
</form>
</div>
</div>
<div x-ref="modal_function" data-ui-modal class="modal box close width-small height-small" data-button-hide="on">
<div>
<form @submit.prevent="addEvent($refs.modal_function)">
<label for="event">
Event
</label>
<select id="event" x-model="selected" @change="setEvent()">
<option value="" selected>Select event</option>
<?php foreach ($patterns as $event) : ?>
<option value="<?php echo $event; ?>"><?php echo $event; ?></option>
<?php endforeach; ?>
</select>
<div x-show="hasResource">
<label x-text="resourceName + ' (optional)'" for="resource"></label>
<input id="resource" type="text" :placeholder="resourceName" x-model="resource" 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="hasSubResource">
<label x-text="subResourceName + ' (optional)'" for="subResource"></label>
<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="hasAttribute">
<label for="attribute">Add Attribute (optional)</label>
<select id="attribute" x-model="attribute">
<option value="*">Select attribute</option>
<template x-for="attr in attributes">
<option :value="attr" x-text="attr"></option>
</template>
</select>
</div>
<button x-show="selected" type="submit">Add Event</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</div>
</li>
</ul>
</div>
@@ -647,7 +703,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
--functionId={{project-function.$id}} \
--activate=true \
--entrypoint='scriptFile' \
--code='/myrepo/myfunction'" data-forms-code="bash" data-lang="bash" data-lang-label="Bash"></textarea>
--code='.'" data-forms-code="bash" data-lang="bash" data-lang-label="Bash"></textarea>
</div>
<p><b>PowerShell</b></p>
@@ -657,7 +713,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
--functionId={{project-function.$id}} `
--activate=true `
--entrypoint='scriptFile' `
--code='/myrepo/myfunction'" data-forms-code="powershell" data-lang="powershell" data-lang-label="PowerShell"></textarea>
--code='.'" data-forms-code="powershell" data-lang="powershell" data-lang-label="PowerShell"></textarea>
</div>
<p>Learn more about <a href="https://appwrite.io/docs/server/functions#functionsCreateDeployment" target="_blank">creating deployments</a>, installing and using the <a href="https://appwrite.io/docs/command-line" target="_blank">Appwrite CLI</a>.</p>
+19 -10
View File
@@ -299,8 +299,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<label for="name">Name <span class="tooltip large" data-tooltip="Choose any name that will help you distinguish between your different apps."><i class="icon-question"></i></span></label>
<input type="text" class="full-width" name="name" required autocomplete="off" placeholder="My Web App" maxlength="128" />
<label for="hostname">Hostname <span class="tooltip large" data-tooltip="The hostname that your website will use to interact with the <?php echo APP_NAME; ?> APIs in production or development environments. No port number required."><i class="icon-question"></i></span></label>
<input name="hostname" type="text" class="margin-bottom" autocomplete="off" placeholder="localhost" required>
<label for="hostname">Hostname <span class="tooltip large" data-tooltip="The hostname that your website will use to interact with the <?php echo APP_NAME; ?> APIs in production or development environments. No protocol or port number required."><i class="icon-question"></i></span></label>
<input name="hostname" type="text" class="margin-bottom" autocomplete="off" placeholder="yourapp.com" required>
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">You can use * to allow wildcard hostnames or subdomains.</div>
<div class="info margin-top margin-bottom">
<div class="text-bold margin-bottom-small">Next Steps</div>
@@ -329,7 +330,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -340,7 +342,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<input type="text" class="full-width" data-ls-attrs="id=name-{{platform.$id}}" name="name" required autocomplete="off" data-ls-bind="{{platform.name}}" placeholder="My Web App" maxlength="128" />
<label for="hostname">Hostname <span class="tooltip large" data-tooltip="The hostname that your website will use to interact with the <?php echo APP_NAME; ?> APIs in production or development environments. No port number required."><i class="icon-question"></i></span></label>
<input name="hostname" type="text" class="margin-bottom" autocomplete="off" placeholder="localhost" data-ls-bind="{{platform.hostname}}" required />
<input name="hostname" type="text" class="margin-bottom" autocomplete="off" placeholder="yourapp.com" data-ls-bind="{{platform.hostname}}" required />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">You can use * to allow wildcard hostnames or subdomains.</div>
<hr />
@@ -714,7 +717,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -746,7 +750,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -777,7 +782,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -808,7 +814,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -841,7 +848,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
@@ -873,7 +881,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
data-success="alert,trigger"
data-success-param-alert-text="Updated platform successfully"
data-success-param-trigger-events="projects.updatePlatform"
data-failure="alert"
data-failure="alert,trigger"
data-failure-param-trigger-events="projects.updatePlatform"
data-failure-param-alert-text="Failed to update platform"
data-failure-param-alert-classname="error">
+4 -4
View File
@@ -532,8 +532,8 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<input name="teamId" type="hidden" data-ls-bind="{{member.teamId}}">
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}/auth/join?project={{router.params.project}}" />
<input name="email" type="hidden" data-ls-bind="{{member.email}}">
<input name="name" type="hidden" data-ls-bind="{{member.name}}">
<input name="email" type="hidden" data-ls-bind="{{member.userEmail}}">
<input name="name" type="hidden" data-ls-bind="{{member.userName}}">
<input name="roles" type="hidden" data-ls-bind="{{member.roles}}" data-cast-to="json">
</form>
</div>
@@ -541,9 +541,9 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<img src="" data-ls-attrs="src={{member|avatar}}" data-size="200" alt="User Avatar" class="avatar pull-start margin-end" loading="lazy" width="60" height="60" />
<div class="margin-bottom-tiny">
<span data-ls-bind="{{member.name}}"></span> &nbsp;&nbsp;<span class="tag" data-ls-bind="{{member.roles.0}}"></span> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="tag red">Pending Approval</span>
<span data-ls-bind="{{member.userName}}"></span> &nbsp;&nbsp;<span class="tag" data-ls-bind="{{member.roles.0}}"></span> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="tag red">Pending Approval</span>
</div>
<span class="text-size-small text-fade" data-ls-bind="{{member.email}}"></span>
<span class="text-size-small text-fade" data-ls-bind="{{member.userEmail}}"></span>
</li>
</ul>
</div>
+52 -6
View File
@@ -111,14 +111,14 @@
<button class="danger">Remove</button>
</form>
<img src="" data-ls-attrs="src={{member|avatar}}" data-size="200" alt="User Avatar" class="avatar pull-start margin-end" loading="lazy" width="60" height="60" />
<img src="" data-ls-attrs="src={{member.userName|avatar}}" data-size="200" alt="User Avatar" class="avatar pull-start margin-end" loading="lazy" width="60" height="60" />
<div class="margin-bottom-tiny">
<a data-ls-attrs="href=/console/users/user?id={{member.userId}}&project={{router.params.project}}"><span data-ls-bind="{{member.name}}"></span></a> &nbsp;&nbsp;
<a data-ls-attrs="href=/console/users/user?id={{member.userId}}&project={{router.params.project}}"><span data-ls-bind="{{member.userName}}"></span></a> &nbsp;&nbsp;
<span data-ls-if="1 == {{member.roles.length}}" class="text-fade tooltip" data-ls-bind="{{member.roles.length}} role" data-ls-attrs="data-tooltip={{member.roles|arraySentence}}"></span>
<span data-ls-if="2 <= {{member.roles.length}}" class="text-fade tooltip" data-ls-bind="{{member.roles.length}} roles" data-ls-attrs="data-tooltip={{member.roles|arraySentence}}"></span>
</div>
<small class="text-size-small text-fade" data-ls-bind="{{member.email}}"></small> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="text-danger text-size-small">Pending Approval</span>
<small class="text-size-small text-fade" data-ls-bind="{{member.userEmail}}"></small> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="text-danger text-size-small">Pending Approval</span>
</li>
</ul>
</div>
@@ -137,12 +137,12 @@
data-analytics-label="Create Team Membership"
data-service="teams.createMembership"
data-event="submit"
data-loading="Sending invitation, please wait..."
data-loading="Adding user, please wait..."
data-success="alert,trigger,reset"
data-success-param-alert-text="Invitation sent successfully"
data-success-param-alert-text="User added successfully"
data-success-param-trigger-events="teams.createMembership"
data-failure="alert"
data-failure-param-alert-text="Failed to send invite"
data-failure-param-alert-text="Failed to add user"
data-failure-param-alert-classname="error">
<input name="teamId" id="team-teamId" type="hidden" data-ls-bind="{{team.$id}}">
@@ -233,7 +233,53 @@
</div>
</div>
</li>
<li data-state="/console/users/teams/team/audit?id={{router.params.id}}&project={{router.params.project}}">
<h2>Activity</h2>
<div
data-service="teams.listLogs"
data-name="logs"
data-param-team-id="{{router.params.id}}"
data-event="load,logs-load">
<div class="box margin-top margin-bottom" data-ls-if="{{logs.logs.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center">
<h3 class="text-bold margin-bottom-no">No logs available.</h3>
</div>
<div class="box" data-ls-if="{{logs.logs.length}} !== 0" style="display: none">
<table class="vertical small">
<thead>
<tr>
<th width="140">Date</th>
<th width="175">Event</th>
<th>Client</th>
<th width="90">Location</th>
<th width="90">IP</th>
</tr>
</thead>
<tbody data-ls-loop="logs.logs" data-ls-as="log">
<tr>
<td data-title="Date: "><span data-ls-bind="{{log.time|dateTime}}"></span></td>
<td data-title="Event: "><span data-ls-bind="{{log.event}}"></span></td>
<td data-title="Client: ">
<img onerror="this.onerror=null;this.className='avatar hide'" data-ls-if="{{log.clientCode|lowercase}} !== 'cli'" data-ls-attrs="src={{env.API}}/avatars/browsers/{{log.clientCode|lowercase}}?width=80&height=80&project={{env.PROJECT}},title={{log.clientName}},alt={{log.clientName}}" class="avatar xxs inline margin-end-small" />
<img onerror="this.onerror=null;this.className='avatar hide'" data-ls-if="{{log.clientCode|lowercase}} === 'cli'" data-ls-attrs="src=/images/clients/terminal.png?buster=<?php echo APP_CACHE_BUSTER; ?>,title={{log.clientName}},alt={{log.clientName}}" class="avatar xxs inline margin-end-small" />
<span data-ls-if="({{log.clientName}})" data-ls-bind="{{log.clientName}} {{log.clientVersion}} on {{log.model}} {{log.osName}} {{log.osVersion}}"></span>
<div data-ls-if="(!{{log.clientName}})" class="text-align-center text-fade">Unknown</div>
</td>
<td data-title="Location: ">
<img onerror="this.onerror=null;this.className='avatar hide'" data-ls-if="{{log.countryCode}} !== '--'" data-ls-attrs="src={{env.API}}/avatars/flags/{{log.countryCode}}?width=80&height=80&project={{env.PROJECT}}" class="avatar xxs inline margin-end-small" />
<span data-ls-bind="{{log.countryName}}"></span>
</td>
<td data-title="IP: "><span data-ls-bind="{{log.ip}}"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</li>
</ul>
</div>
</div>
+101
View File
@@ -242,6 +242,52 @@
<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>
</ul>
<div data-ls-if="{{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-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="emailVerification" value="true" data-cast-to="boolean">
<button type="submit" class="dark fill">Verify Account</button>
</form>
</div>
<div data-ls-if="{{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-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="emailVerification" value="false" data-cast-to="boolean">
<button type="submit" class="dark fill">Unverify Account</button>
</form>
</div>
<div data-ls-if="{{user.status}} === true" style="display: none">
<form name="users.updateStatus" class="margin-bottom"
data-analytics
@@ -286,6 +332,61 @@
</div>
</div>
</li>
<li data-state="/console/users/user/memberships?id={{router.params.id}}&project={{router.params.project}}">
<h2>Memberships</h2>
<div
data-service="users.getMemberships"
data-name="memberships"
data-param-user-id="{{router.params.id}}"
data-event="load,users.update,teams.deleteMembership">
<div class="box margin-top margin-bottom" data-ls-if="{{memberships.memberships.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center">
<h3 class="text-bold margin-bottom-no">No memberships available.</h3>
</div>
<div data-ls-if="0 != {{memberships.total}}">
<div class="margin-bottom-small margin-end-small text-align-end text-size-small margin-top-negative text-fade"><span data-ls-bind="{{memberships.total}}"></span> memberships found</div>
<div class="box margin-bottom">
<ul data-ls-loop="memberships.memberships" data-ls-as="membership" class="list">
<li class="clear">
<form class="pull-end"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Delete Team Membership"
data-service="teams.deleteMembership"
data-event="submit"
data-success="alert,trigger"
data-confirm="Are you sure you want to remove that user from the team?"
data-success-param-alert-text="Member Removed Successfully"
data-success-param-trigger-events="teams.deleteMembership"
data-failure="alert"
data-failure-param-alert-text="Failed to Remove Member"
data-failure-param-alert-classname="error">
<input name="teamId" data-ls-attrs="id=leave-teamId-{{membership.teamId}}" type="hidden" data-ls-bind="{{membership.teamId}}">
<input name="membershipId" data-ls-attrs="id=leave-membershipId-{{membership.$id}}" type="hidden" data-ls-bind="{{membership.$id}}">
<button class="danger">Remove</button>
</form>
<div class="pull-start margin-end">
<img src="" data-ls-attrs="src={{membership.teamName|avatar}},alt={{membership.teamName}}" data-size="60" class="avatar margin-end pull-start" loading="lazy" width="60" height="60" />
</div>
<a data-ls-attrs="href=/console/users/teams/team?id={{membership.teamId}}&project={{router.params.project}}" data-ls-bind="{{membership.teamName}}"></a>
<div class="margin-top-small">
<span data-ls-if="1 == {{membership.roles.length}}" class="text-fade tooltip" data-ls-bind="{{membership.roles.length}} role" data-ls-attrs="data-tooltip={{membership.roles|arraySentence}}"></span>
<span data-ls-if="2 <= {{membership.roles.length}}" class="text-fade tooltip" data-ls-bind="{{membership.roles.length}} roles" data-ls-attrs="data-tooltip={{membership.roles|arraySentence}}"></span>
<span data-ls-if="false === {{membership.confirm}}" class="text-danger text-size-small">Pending Approval</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</li>
<li data-state="/console/users/user/sessions?id={{router.params.id}}&project={{router.params.project}}">
<h2>Sessions</h2>
+23 -201
View File
@@ -1,8 +1,28 @@
<?php
$events = array_keys($this->getParam('events', []));
$events = $this->getParam('events', []);
$patterns = [];
foreach ($events as $name => $event) {
foreach ($event as $key => $value) {
if (!\str_starts_with($key, '$')) {
if (!($value['$resource'] ?? false)) {
$patterns[] = "{$name}.{$key}";
} else {
foreach ($value as $key2 => $value2) {
if (!\str_starts_with($key2, '$')) {
if (!($value2['$resource'] ?? false)) {
$patterns[] = "{$key}.{$key2}";
}
}
}
}
}
}
}
?>
<div class="cover margin-bottom-large">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/home?project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> Home</a>
@@ -30,117 +50,7 @@ $events = array_keys($this->getParam('events', []));
<div class="box margin-bottom" data-ls-if="0 != {{console-webhooks.total}}">
<ul data-ls-loop="console-webhooks.webhooks" data-ls-as="webhook" class="list">
<li class="clear">
<div data-ui-modal class="modal close sticky-footer" data-button-text="Update" data-button-class="pull-end">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Update Webhook</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Update Project Webhook"
data-service="projects.updateWebhook"
data-scope="console"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Updated webhook successfully"
data-success-param-trigger-events="projects.updateWebhook"
data-failure="alert"
data-failure-param-alert-text="Failed to update 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="{{webhook.$id}}" />
<label data-ls-attrs="for=name-{{webhook.$id}}">Name</label>
<input type="text" class="full-width" data-ls-attrs="id=name-{{webhook.$id}}" name="name" required autocomplete="off" data-ls-bind="{{webhook.name}}" maxlength="128" />
<label data-ls-attrs="for=url-{{webhook.$id}}">POST URL</label>
<input type="url" class="full-width" data-ls-attrs="id=url-{{webhook.$id}}" name="url" required autocomplete="off" placeholder="https://example.com/callback" data-ls-bind="{{webhook.url}}" />
<section data-forms-select-all>
<label data-ls-attrs="for=events-{{webhook.$id}}">Events</label>
<div class="row responsive thin">
<?php foreach ($events as $i => $event): ?>
<div class="col span-6 text-one-liner margin-bottom text-height-large text-size-small" title="<?php echo $event; ?>">
<input type="checkbox" name="events" data-ls-bind="{{webhook.events}}" id="update-<?php echo $event; ?>" value="<?php echo $event; ?>" data-by-key="true" />
&nbsp;
<label class="inline" for="update-<?php echo $event; ?>"><?php echo $event; ?></label>
</div>
<?php if (($i + 1) % 2 === 0): ?>
</div>
<div class="row responsive thin">
<?php endif;?>
<?php endforeach;?>
</div>
</section>
<div class="margin-bottom toggle" data-ls-ui-open data-button-aria="Advanced Options">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<h2 class="margin-bottom">
Advanced Options
<small class="text-size-small">(optional)</small>
</h2>
<label data-ls-attrs="for=security-{{task.$id}}" class="margin-bottom-small">
<div class="margin-bottom-small text-bold">SSL / TLS (Certificate verification)</div>
<input name="security" type="radio" required data-ls-attrs="id=secure-yes-{{webhook.$id}}" data-ls-bind="{{webhook.security}}" value="true" data-cast-to="boolean" /> &nbsp; <span>Enabled</span> &nbsp;
<input name="security" type="radio" required data-ls-attrs="id=secure-no-{{webhook.$id}}" data-ls-bind="{{webhook.security}}" value="false" data-cast-to="boolean" /> &nbsp; <span>Disabled</span> &nbsp;
</label>
<p class="margin-bottom text-size-small text-fade"><span class="text-red">Warning</span>: Untrusted or self-signed certificates may not be secure.
<a href="https://en.wikipedia.org/wiki/Self-signed_certificate" target="_blank" rel="noopener">Learn more<i class="icon-link-ext"></i></a>
</p>
<label class="text-bold">HTTP Authentication <span class="tooltip" data-tooltip="Use to secure your endpoint from untrusted sources"><i class="icon-question"></i></span> &nbsp;<small>(optional)</small></label>
<div class="row responsive thin">
<div class="col span-6 margin-bottom">
<label data-ls-attrs="for=httpUser-{{webhook.$id}}">User</label>
<input type="text" class="full-width margin-bottom-no" data-ls-attrs="id=httpUser-{{webhook.$id}}" name="httpUser" autocomplete="off" data-ls-bind="{{webhook.httpUser}}" />
</div>
<div class="col span-6 margin-bottom">
<label data-ls-attrs="for=httpPass-{{webhook.$id}}">Password</label>
<input type="password" data-forms-show-secret class="full-width margin-bottom-no" data-ls-attrs="id=httpPass-{{webhook.$id}}" name="httpPass" autocomplete="off" data-ls-bind="{{webhook.httpPass}}" />
</div>
</div>
</div>
<footer>
<button type="submit">Update</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<form class="pull-end margin-end"
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,trigger"
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="{{webhook.$id}}" />
<button class="danger reverse">Delete</button>
</form>
<a data-ls-attrs="href=/console/webhooks/webhook?project={{router.params.project}}&id={{webhook.$id}}" class="button reverse pull-end margin-end">Manage</a>
<span data-ls-bind="{{webhook.name}}"></span> &nbsp; (<span data-ls-bind="{{webhook.events.length}}"></span> events)
<span data-ls-if="false === {{webhook.security}}">
@@ -152,93 +62,5 @@ $events = array_keys($this->getParam('events', []));
</li>
</ul>
</div>
<div class="clear">
<div data-ui-modal class="modal close box sticky-footer" data-button-text="Add Webhook">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Add Webhook</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Project Webhook"
data-service="projects.createWebhook"
data-scope="console"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created webhook successfully"
data-success-param-trigger-events="projects.createWebhook"
data-failure="alert"
data-failure-param-alert-text="Failed to create webhook"
data-failure-param-alert-classname="error">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<label for="name">Name</label>
<input type="text" class="full-width" id="name" name="name" required autocomplete="off" maxlength="128" />
<label for="url">POST URL</label>
<input type="url" class="full-width" id="url" name="url" required autocomplete="off" placeholder="https://example.com/callback" />
<section data-forms-select-all>
<label for="events">Events</label>
<div class="row responsive thin">
<?php foreach ($events as $i => $event): ?>
<div class="col span-6 text-one-liner margin-bottom text-height-large text-size-small" title="<?php echo $event; ?>">
<input type="checkbox" name="events" id="add-<?php echo $event; ?>" value="<?php echo $event; ?>" data-by-key="true" />
&nbsp;
<label class="inline" for="add-<?php echo $event; ?>"><?php echo $event; ?></label>
</div>
<?php if (($i + 1) % 2 === 0): ?>
</div>
<div class="row responsive thin">
<?php endif;?>
<?php endforeach;?>
</div>
</section>
<div class="margin-bottom toggle" data-ls-ui-open data-button-aria="Advanced Options">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<h2 class="margin-bottom">
Advanced Options
<small class="text-size-small">(optional)</small>
</h2>
<label data-ls-attrs="for=security-{{task.$id}}" class="margin-bottom-small">
<div class="margin-bottom-small text-bold">SSL / TLS (Certificate verification)</div>
<input name="security" type="radio" required data-ls-attrs="id=secure-yes" checked="checked" value="1" /> &nbsp; <span>Enabled</span> &nbsp;
<input name="security" type="radio" required data-ls-attrs="id=secure-no" value="0" /> &nbsp; <span>Disabled</span> &nbsp;
</label>
<p class="margin-bottom text-size-small text-fade"><span class="text-red">Warning</span>: Untrusted or self-signed certificates may not be secure.
<a href="https://en.wikipedia.org/wiki/Self-signed_certificate" target="_blank" rel="noopener">Learn more<i class="icon-link-ext"></i></a>
</p>
<label class="text-bold">HTTP Authentication <span class="tooltip" data-tooltip="Use to secure your endpoint from untrusted sources"><i class="icon-question"></i></span> &nbsp;<small>(optional)</small></label>
<div class="row responsive thin">
<div class="col span-6 margin-bottom">
<label for="httpUser">User</label>
<input type="text" class="full-width margin-bottom-no" id="httpUser" name="httpUser" autocomplete="off" />
</div>
<div class="col span-6 margin-bottom">
<label for="httpPass">Password</label>
<input type="password" data-forms-show-secret class="full-width margin-bottom-no" id="httpPass" name="httpPass" autocomplete="off" />
</div>
</div>
</div>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
</div>
<a data-ls-attrs="href=/console/webhooks/webhook/new?project={{router.params.project}}" class="button">Add Webhook</a>
</div>
+205
View File
@@ -0,0 +1,205 @@
<?php
$new = $this->getParam('new', false);
$events = $this->getParam('events', []);
$patterns = [];
foreach ($events as $name => $event) {
$patterns[] = $name;
foreach ($event as $key => $value) {
if (!\str_starts_with($key, '$')) {
if (!($value['$resource'] ?? false)) {
$patterns[] = "{$name}.{$key}";
} else {
$patterns[] = $key;
foreach ($value as $key2 => $value2) {
if (!\str_starts_with($key2, '$')) {
if (!($value2['$resource'] ?? false)) {
$patterns[] = "{$key}.{$key2}";
}
}
}
}
}
}
}
sort($patterns);
?>
<div
data-service="projects.getWebhook"
data-name="project-webhook"
data-scope="console"
data-event="load,projects.createWebhook, projects.deleteWebhook, projects.updateWebhook"
data-param-project-id="{{router.params.project}}"
data-param-webhook-id="{{router.params.id}}"
data-success="trigger"
data-success-param-trigger-events="projects.getWebhook">
<div class="cover">
<h1 class="zone xl margin-bottom-large">
<a data-ls-attrs="href=/console/webhooks?project={{router.params.project}}" class="back text-size-small link-return-animation--start"><i class="icon-left-open"></i> Webhooks</a>
<br />
<?php if ($new) : ?>
<span>Add Webhook</span>
<? else : ?>
<span data-ls-bind="{{project-webhook.name}}&nbsp;">&nbsp;</span>
<?php endif; ?>
</h1>
</div>
<div class="zone xl" x-data="events">
<h2 class="margin-top">Settings</h2>
<div class="row responsive">
<div class="col span-8 margin-bottom">
<label>&nbsp;</label>
<div class="box margin-bottom-large">
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
<?php if ($new) : ?>
data-success="alert,trigger,redirect"
data-analytics-label="Create Project Webhook"
data-service="projects.createWebhook"
data-success-param-alert-text="Created webhook successfully"
data-success-param-trigger-events="projects.createWebhook"
data-failure-param-alert-text="Failed to create webhook"
data-success-param-redirect-url="/console/webhooks?project={{router.params.project}}"
<?php else : ?>
data-success="alert,trigger"
data-analytics-label="Update Project Webhook"
data-service="projects.updateWebhook"
data-success-param-alert-text="Updated webhook successfully"
data-success-param-trigger-events="projects.updateWebhook"
data-failure-param-alert-text="Failed to update webhook"
<?php endif; ?>
data-scope="console"
data-event="submit"
data-failure="alert"
data-failure-param-alert-classname="error">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<?php if (!$new) : ?><input type="hidden" name="webhookId" data-ls-bind="{{project-webhook.$id}}" /><?php endif; ?>
<label data-ls-attrs="for=name-{{webhook.$id}}">Name</label>
<input type="text" class="full-width" data-ls-attrs="id=name-{{project-webhook.$id}}" name="name" required autocomplete="off" data-ls-bind="{{project-webhook.name}}" maxlength="128" />
<label data-ls-attrs="for=url-{{webhook.$id}}">POST URL</label>
<input type="url" class="full-width" data-ls-attrs="id=url-{{project-webhook.$id}}" name="url" required autocomplete="off" placeholder="https://example.com/callback" data-ls-bind="{{project-webhook.url}}" />
<section class="margin-bottom-small" data-ls-attrs="x-init=load({{project-webhook.events}})">
<label class="margin-bottom-small">Events <span class="tooltip small" data-tooltip="Set events that will trigger your webhook."><i class="icon-info-circled"></i></span></label>
<div>
<template x-for="event in Array.from(events)">
<div class="row events responsive thin margin-bottom-small">
<div class="col span-12 margin-bottom-small">
<span class="text" x-text="event"></span>
<span class="action" @click="removeEvent(event)">
<i class="icon-trash"></i>
</span>
</div>
<input name="events" data-cast-to="array" type="hidden" :value="event"></input>
</div>
</template>
</div>
<button class="margin-end margin-bottom-small reverse" type="button" @click="showModal($refs.modal_webhook)">Add Event</button>
</section>
<div class="margin-bottom toggle" data-ls-ui-open data-button-aria="Advanced Options">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<h2 class="margin-bottom">
Advanced Options
<small class="text-size-small">(optional)</small>
</h2>
<label data-ls-attrs="for=security-{{project-webhook.$id}}" class="margin-bottom-small">
<div class="margin-bottom-small">SSL / TLS (Certificate verification)</div>
<input <?php if ($new): ?>checked<?php else: ?>data-ls-bind="{{project-webhook.security}}"<?php endif; ?> name="security" type="radio" required data-ls-attrs="id=secure-yes-{{project-webhook.$id}}" value="true" data-cast-to="boolean" /> &nbsp; <span>Enabled</span> &nbsp;
<input name="security" type="radio" required data-ls-attrs="id=secure-no-{{project-webhook.$id}}" data-ls-bind="{{project-webhook.security}}" value="false" data-cast-to="boolean" /> &nbsp; <span>Disabled</span> &nbsp;
</label>
<p class="margin-bottom text-size-small text-fade"><span class="text-red">Warning</span>: Untrusted or self-signed certificates may not be secure.
<a href="https://en.wikipedia.org/wiki/Self-signed_certificate" target="_blank" rel="noopener">Learn more<i class="icon-link-ext"></i></a>
</p>
<label>HTTP Authentication <span class="tooltip" data-tooltip="Use to secure your endpoint from untrusted sources"><i class="icon-question"></i></span> &nbsp;<small>(optional)</small></label>
<div class="row responsive thin">
<div class="col span-6 margin-bottom">
<label data-ls-attrs="for=httpUser-{{project-webhook.$id}}">User</label>
<input type="text" class="full-width margin-bottom-no" data-ls-attrs="id=httpUser-{{project-webhook.$id}}" name="httpUser" autocomplete="off" data-ls-bind="{{project-webhook.httpUser}}" />
</div>
<div class="col span-6 margin-bottom">
<label data-ls-attrs="for=httpPass-{{project-webhook.$id}}">Password</label>
<input type="password" data-forms-show-secret class="full-width margin-bottom-no" data-ls-attrs="id=httpPass-{{project-webhook.$id}}" name="httpPass" autocomplete="off" data-ls-bind="{{project-webhook.httpPass}}" />
</div>
</div>
</div>
<footer>
<button type="submit">Update</button>
</footer>
</form>
</div>
</div>
<?php if (!$new) : ?>
<div class="col span-4 sticky-top margin-bottom">
<label>Webhook ID</label>
<div class="input-copy margin-bottom">
<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">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="webhookId" data-ls-bind="{{project-webhook.$id}}" />
<button class="danger fill">Delete Webhook</button>
</form>
</div>
<?php endif; ?>
<div x-ref="modal_webhook" data-ui-modal class="modal box close width-small height-small" data-button-hide="on">
<div>
<form @submit.prevent="addEvent($refs.modal_webhook)">
<label for="event">
Event
</label>
<select id="event" x-model="selected" @change="setEvent()">
<option value="" selected>Select event</option>
<?php foreach ($patterns as $event) : ?>
<option value="<?php echo $event; ?>"><?php echo $event; ?></option>
<?php endforeach; ?>
</select>
<div x-show="hasResource">
<label x-text="resourceName + ' (optional)'" for="resource"></label>
<input id="resource" type="text" :placeholder="resourceName" x-model="resource" 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="hasSubResource">
<label x-text="subResourceName + ' (optional)'" for="subResource"></label>
<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="hasAttribute">
<label for="attribute">Add Attribute (optional)</label>
<select id="attribute" x-model="attribute">
<option value="*">Select attribute</option>
<template x-for="attr in attributes">
<option :value="attr" x-text="attr"></option>
</template>
</select>
</div>
<button x-show="selected" type="submit">Add Event</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</div>
</div>
</div>
</div>
+48 -4
View File
@@ -2,9 +2,8 @@ x-logging: &x-logging
logging:
driver: 'json-file'
options:
max-file: 5
max-size: 10m
max-file: '5'
max-size: '10m'
<?php
$httpPort = $this->getParam('httpPort', '');
@@ -16,8 +15,9 @@ $image = $this->getParam('image', '');
services:
traefik:
image: traefik:2.5
image: traefik:2.7
container_name: appwrite-traefik
<<: *x-logging
command:
- --providers.file.directory=/storage/config
- --providers.file.watch=true
@@ -119,6 +119,18 @@ services:
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_FUNCTIONS_SIZE_LIMIT
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_BUILD_TIMEOUT
@@ -266,6 +278,18 @@ services:
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
@@ -342,6 +366,7 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
@@ -430,6 +455,18 @@ services:
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
- _APP_STORAGE_BACKBLAZE_SECRET
- _APP_STORAGE_BACKBLAZE_REGION
- _APP_STORAGE_BACKBLAZE_BUCKET
- _APP_STORAGE_LINODE_ACCESS_KEY
- _APP_STORAGE_LINODE_SECRET
- _APP_STORAGE_LINODE_REGION
- _APP_STORAGE_LINODE_BUCKET
- _APP_STORAGE_WASABI_ACCESS_KEY
- _APP_STORAGE_WASABI_SECRET
- _APP_STORAGE_WASABI_REGION
- _APP_STORAGE_WASABI_BUCKET
- DOCKERHUB_PULL_USERNAME
- DOCKERHUB_PULL_PASSWORD
@@ -473,10 +510,17 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
+1 -1
View File
@@ -139,7 +139,7 @@ if(!empty($platforms)) {
<span data-ls-bind="{{alert.text}}"></span>
<span data-ls-if="undefined !== {{alert.link}}">
<a data-ls-attrs="href={{alert.link}}" data-ls-bind="{{alert.label}}" data-remove></a>
<a data-ls-attrs="href={{alert.link}}" data-ls-bind="{{alert.label}}" target="_blank" data-remove></a>
</span>
</div>
</li>
+30 -18
View File
@@ -1,17 +1,20 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Resque\Worker;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Database\Document;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Audits V1 Worker');
Console::success(APP_NAME . ' audits worker v1 has started');
class AuditsV1 extends Worker
{
public function getName(): string {
public function getName(): string
{
return "audits";
}
@@ -21,30 +24,39 @@ class AuditsV1 extends Worker
public function run(): void
{
$projectId = $this->args['projectId'];
$userId = $this->args['userId'];
$userName = $this->args['userName'];
$userEmail = $this->args['userEmail'];
$events = $this->args['events'];
$payload = $this->args['payload'];
$mode = $this->args['mode'];
$event = $this->args['event'];
$resource = $this->args['resource'];
$userAgent = $this->args['userAgent'];
$ip = $this->args['ip'];
$data = $this->args['data'];
$dbForProject = $this->getProjectDB($projectId);
$audit = new Audit($dbForProject);
$audit->log($userId, $event, $resource, $userAgent, $ip, '', [
'userName' => $userName,
'userEmail' => $userEmail,
'mode' => $mode,
'data' => $data,
]);
$user = new Document($this->args['user']);
$project = new Document($this->args['project']);
$userName = $user->getAttribute('name', '');
$userEmail = $user->getAttribute('email', '');
$dbForProject = $this->getProjectDB($project->getId());
$audit = new Audit($dbForProject);
$audit->log(
userId: $user->getId(),
// Pass first, most verbose event pattern
event: $events[0],
resource: $resource,
userAgent: $userAgent,
ip: $ip,
location: '',
data: [
'userName' => $userName,
'userEmail' => $userEmail,
'mode' => $mode,
'data' => $payload,
]
);
}
public function shutdown(): void
{
// ... Remove environment for this job
}
}
+72 -41
View File
@@ -1,7 +1,9 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Appwrite\Utopia\Response\Model\Deployment;
use Cron\CronExpression;
use Executor\Executor;
use Utopia\Database\Validator\Authorization;
@@ -11,43 +13,41 @@ use Utopia\Storage\Storage;
use Utopia\Database\Document;
use Utopia\Config\Config;
require_once __DIR__.'/../init.php';
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');
Console::success(APP_NAME . ' build worker v1 has started');
// TODO: Executor should return appropriate response codes.
class BuildsV1 extends Worker
{
/**
* @var Executor
*/
private $executor = null;
{
private ?Executor $executor = null;
public function getName(): string
public function getName(): string
{
return "builds";
}
public function init(): void {
public function init(): void
{
$this->executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
}
public function run(): void
{
$type = $this->args['type'] ?? '';
$projectId = $this->args['projectId'] ?? '';
$functionId = $this->args['resourceId'] ?? '';
$deploymentId = $this->args['deploymentId'] ?? '';
$project = new Document($this->args['project'] ?? []);
$resource = new Document($this->args['resource'] ?? []);
$deployment = new Document($this->args['deployment'] ?? []);
switch ($type) {
case BUILD_TYPE_DEPLOYMENT:
case BUILD_TYPE_RETRY:
Console::info("Creating build for deployment: $deploymentId");
$this->buildDeployment($projectId, $functionId, $deploymentId);
Console::info('Creating build for deployment: ' . $deployment->getId());
$this->buildDeployment($project, $resource, $deployment);
break;
default:
@@ -56,18 +56,16 @@ class BuildsV1 extends Worker
}
}
protected function buildDeployment(string $projectId, string $functionId, string $deploymentId)
protected function buildDeployment(Document $project, Document $function, Document $deployment)
{
$dbForProject = $this->getProjectDB($projectId);
$dbForConsole = $this->getConsoleDB();
$project = $dbForConsole->getDocument('projects', $projectId);
$function = $dbForProject->getDocument('functions', $functionId);
$dbForProject = $this->getProjectDB($project->getId());
$function = $dbForProject->getDocument('functions', $function->getId());
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$deployment = $dbForProject->getDocument('deployments', $deployment->getId());
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404);
}
@@ -89,7 +87,7 @@ class BuildsV1 extends Worker
'$read' => [],
'$write' => [],
'startTime' => $startTime,
'deploymentId' => $deploymentId,
'deploymentId' => $deployment->getId(),
'status' => 'processing',
'outputPath' => '',
'runtime' => $function->getAttribute('runtime'),
@@ -101,7 +99,7 @@ class BuildsV1 extends Worker
'duration' => 0
]));
$deployment->setAttribute('buildId', $buildId);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
$build = $dbForProject->getDocument('builds', $buildId);
}
@@ -110,12 +108,39 @@ class BuildsV1 extends Worker
$build->setAttribute('status', 'building');
$build = $dbForProject->updateDocument('builds', $buildId, $build);
/** Send realtime event */
$target = Realtime::fromPayload('functions.deployments.update', $build, $project);
/** Trigger Webhook */
$deploymentModel = new Deployment();
$deploymentUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME);
$deploymentUpdate
->setProject($project)
->setEvent('functions.[functionId].deployments.[deploymentId].update')
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())))
->trigger();
/** Trigger Functions */
$deploymentUpdate
->setClass(Event::FUNCTIONS_CLASS_NAME)
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
->trigger();
/** Trigger Realtime */
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId()
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
event: 'functions.deployments.update',
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
@@ -126,13 +151,13 @@ class BuildsV1 extends Worker
try {
$response = $this->executor->createRuntime(
projectId: $projectId,
deploymentId: $deploymentId,
projectId: $project->getId(),
deploymentId: $deployment->getId(),
entrypoint: $deployment->getAttribute('entrypoint'),
source: $source,
destination: APP_STORAGE_BUILDS . "/app-$projectId",
vars: $vars,
runtime: $key,
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
vars: $vars,
runtime: $key,
baseImage: $baseImage,
workdir: '/usr/code',
remove: true,
@@ -149,14 +174,14 @@ class BuildsV1 extends Worker
$build->setAttribute('status', $response['status']);
$build->setAttribute('outputPath', $response['outputPath']);
$build->setAttribute('stderr', $response['stderr']);
$build->setAttribute('stdout', $response['stdout']);
$build->setAttribute('stdout', $response['response']);
Console::success("Build id: $buildId created");
/** Set auto deploy */
if ($deployment->getAttribute('activate') === true) {
$function->setAttribute('deployment', $deployment->getId());
$function = $dbForProject->updateDocument('functions', $functionId, $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
}
/** Update function schedule */
@@ -164,8 +189,7 @@ class BuildsV1 extends Worker
$cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null;
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
$function->setAttribute('scheduleNext', (int)$next);
$function = $dbForProject->updateDocument('functions', $functionId, $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
} catch (\Throwable $th) {
$endtime = \time();
$build->setAttribute('endTime', $endtime);
@@ -176,19 +200,26 @@ class BuildsV1 extends Worker
} finally {
$build = $dbForProject->updateDocument('builds', $buildId, $build);
/**
* Send realtime Event
/**
* Send realtime Event
*/
$target = Realtime::fromPayload('functions.deployments.update', $build, $project);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
event: 'functions.deployments.update',
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
}
public function shutdown(): void {}
public function shutdown(): void
{
}
}
+369 -152
View File
@@ -1,22 +1,32 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Certificates V1 Worker');
Console::success(APP_NAME . ' certificates worker v1 has started');
class CertificatesV1 extends Worker
{
public function getName(): string {
/**
* Database connection shared across all methods of this file
*
* @var Database
*/
private Database $dbForConsole;
public function getName(): string
{
return "certificates";
}
@@ -26,172 +36,379 @@ class CertificatesV1 extends Worker
public function run(): void
{
$dbForConsole = $this->getConsoleDB();
Authorization::disable();
Authorization::setDefaultStatus(false);
/**
* 1. Get new domain document - DONE
* 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE
* 2. Check if a certificate already exists - DONE
* 3. Check if certificate is about to expire, if not - skip it
* 3.1. Create / renew certificate
* 3.2. Update loadblancer
* 3.3. Update database (domains, change date, expiry)
* 3.4. Set retry on failure
* 3.5. Schedule to renew certificate in 60 days
* 1. Read arguments and validate domain
* 2. Get main domain
* 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain)
* 4. Validate security email. Cannot be empty, required by LetsEncrypt
* 5. Validate renew date with certificate file, unless requested to skip by parameter
* 6. Issue a certificate using certbot CLI
* 7. Update 'log' attribute on certificate document with Certbot message
* 8. Create storage folder for certificate, if not ready already
* 9. Move certificates from Certbot location to our Storage
* 10. Create/Update our Storage with new Traefik config with new certificate paths
* 11. Read certificate file and update 'renewDate' on certificate document
* 12. Update 'issueDate' and 'attempts' on certificate
*
* If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker
*
* If code stops with expected error:
* 1. 'log' attribute on document is updated with error message
* 2. 'attempts' amount is increased
* 3. Console log is shown
* 4. Email is sent to security email
*
* Unless unexpected error occurs, at the end, we:
* 1. Update 'updated' attribute on document
* 2. Save document to database
* 3. Update all domains documents with current certificate ID
*
* Note: Renewals are checked and scheduled from maintenence worker
*/
Authorization::disable();
$this->dbForConsole = $this->getConsoleDB();
// Args
$document = $this->args['document'];
$domain = $this->args['domain'];
$skipCheck = $this->args['skipRenewCheck'] ?? false; // If true, we won't double-check expiry from cert file
$document = new Document($this->args['domain'] ?? []);
$domain = new Domain($document->getAttribute('domain', ''));
// Validation Args
$validateTarget = $this->args['validateTarget'] ?? true;
$validateCNAME = $this->args['validateCNAME'] ?? true;
// Options
$domain = new Domain((!empty($domain)) ? $domain : '');
$expiry = 60 * 60 * 24 * 30 * 2; // 60 days
$safety = 60 * 60; // 1 hour
$renew = (\time() + $expiry);
if (empty($domain->get())) {
throw new Exception('Missing domain');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new Exception('Unknown public suffix for domain');
}
if ($validateTarget) {
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if(!$target->isKnown() || $target->isTest()) {
throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.');
}
}
if ($validateCNAME) {
$validator = new CNAME($target->get()); // Verify Domain with DNS records
if(!$validator->isValid($domain->get())) {
throw new Exception('Failed to verify domain DNS records');
}
}
$certificate = $dbForConsole->findOne('certificates', [
new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()])
]);
// $condition = ($certificate
// && $certificate instanceof Document
// && isset($certificate['issueDate'])
// && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false';
// throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition);
$certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : [];
if (
!empty($certificate)
&& isset($certificate['issueDate'])
&& (($certificate['issueDate'] + ($expiry)) > \time())
) { // Check last issue time
throw new Exception('Renew isn\'t required');
}
$staging = (App::isProduction()) ? '' : ' --dry-run';
$email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate');
}
$stdout = '';
$stderr = '';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain->get()}", '', $stdout, $stderr);
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
$path = APP_STORAGE_CERTIFICATES . '/' . $domain->get();
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path...');
}
}
if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout));
}
if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem: ' . \json_encode($stdout));
}
$certificate = new Document(\array_merge($certificate, [
'domain' => $domain->get(),
'issueDate' => \time(),
'renewDate' => $renew,
'attempts' => 0,
'log' => \json_encode($stdout),
]));
$certificate = $dbForConsole->createDocument('certificates', $certificate);
// Get current certificate
$certificate = $this->dbForConsole->findOne('certificates', [new Query('domain', Query::TYPE_EQUAL, [$domain->get()])]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if (!$certificate) {
throw new Exception('Failed saving certificate to DB');
$certificate = new Document();
$certificate->setAttribute('domain', $domain->get());
}
if(!empty($document)) {
$certificate = new Document(\array_merge($document, [
'updated' => \time(),
'certificateId' => $certificate->getId(),
try {
// Email for alerts is required by LetsEncrypt
$email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.');
}
// Validate domain and DNS records. Skip if job is forced
if (!$skipCheck) {
$mainDomain = $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
$this->validateDomain($domain, $isMainDomain);
}
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$skipCheck && !$this->isRenewRequired($domain->get())) {
throw new Exception('Renew isn\'t required.');
}
// Generate certificate files using Let's Encrypt
$letsEncryptData = $this->issueCertificate($domain->get(), $email);
// Command succeeded, store all data into document
// We store stderr too, because it may include warnings
$certificate->setAttribute('log', \json_encode([
'stdout' => $letsEncryptData['stdout'],
'stderr' => $letsEncryptData['stderr'],
]));
$certificate = $dbForConsole->updateDocument('domains', $certificate->getId(), $certificate);
// Give certificates to Traefik
$this->applyCertificateFiles($domain->get(), $letsEncryptData);
if(!$certificate) {
throw new Exception('Failed saving domain to DB');
}
// Update certificate info stored in database
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
$certificate->setAttribute('attempts', 0);
$certificate->setAttribute('issueDate', \time());
} catch (Throwable $e) {
// Set exception as log in certificate document
$certificate->setAttribute('log', $e->getMessage());
// Increase attempts count
$attempts = $certificate->getAttribute('attempts', 0) + 1;
$certificate->setAttribute('attempts', $attempts);
// Send email to security email
$this->notifyError($domain->get(), $e->getMessage(), $attempts);
} finally {
// All actions result in new updatedAt date
$certificate->setAttribute('updated', \time());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate);
}
$config =
"tls:
certificates:
- certFile: /storage/certificates/{$domain->get()}/fullchain.pem
keyFile: /storage/certificates/{$domain->get()}/privkey.pem";
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) {
throw new Exception('Failed to save SSL configuration');
}
ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [
'document' => [],
'domain' => $domain->get(),
'validateTarget' => $validateTarget,
'validateCNAME' => $validateCNAME,
]); // Async task rescheduale
Authorization::reset();
}
public function shutdown(): void
{
}
/**
* Save certificate data into database.
*
* @param string $domain Domain name that certificate is for
* @param Document $certificate Certificate document that we need to save
*
* @return void
*/
private function saveCertificateDocument(string $domain, Document $certificate): void
{
// Check if update or insert required
$certificateDocument = $this->dbForConsole->findOne('certificates', [new Query('domain', Query::TYPE_EQUAL, [$domain])]);
if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
$certificate = $this->dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate);
} else {
$certificate = $this->dbForConsole->createDocument('certificates', $certificate);
}
$certificateId = $certificate->getId();
$this->updateDomainDocuments($certificateId, $domain);
}
/**
* Get main domain. Needed as we do different checks for main and non-main domains.
*
* @return null|string Returns main domain. If null, there is no main domain yet.
*/
private function getMainDomain(): ?string
{
$envDomain = App::getEnv('_APP_DOMAIN', '');
if (!empty($envDomain) && $envDomain !== 'localhost') {
return $envDomain;
} else {
$domainDocument = $this->dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
if ($domainDocument) {
return $domainDocument->getAttribute('domain');
}
}
return null;
}
/**
* Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check:
* - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt)
* - Domain must have proper DNS record
*
* @param Domain $domain Domain which we validate
* @param bool $isMainDomain In case of master domain, we look for different DNS configurations
*
* @return void
*/
private function validateDomain(Domain $domain, bool $isMainDomain): void
{
if (empty($domain->get())) {
throw new Exception('Missing certificate domain.');
}
if (!$domain->isKnown() || $domain->isTest()) {
throw new Exception('Unknown public suffix for domain.');
}
if (!$isMainDomain) {
// TODO: Would be awesome to also support A/AAAA records here. Maybe dry run?
// Validate if domain target is properly configured
$target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', ''));
if (!$target->isKnown() || $target->isTest()) {
throw new Exception('Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
}
// Verify domain with DNS records
$validator = new CNAME($target->get());
if (!$validator->isValid($domain->get())) {
throw new Exception('Failed to verify domain DNS records.');
}
} else {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
}
}
/**
* Reads expiry date of certificate from file and decides if renewal is required or not.
*
* @param string $domain Domain for which we check certificate file
*
* @return bool True, if certificate needs to be renewed
*/
private function isRenewRequired(string $domain): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
$validTo = null;
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
return false;
}
}
return true;
}
/**
* LetsEncrypt communication to issue certificate (using certbot CLI)
*
* @param string $domain Domain to generate certificate for
*
* @return array Named array with keys 'stdout' and 'stderr', both string
*/
private function issueCertificate(string $domain, string $email): array
{
$stdout = '';
$stderr = '';
$staging = (App::isProduction()) ? '' : ' --dry-run';
$exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}"
. " --email " . $email
. " -w " . APP_STORAGE_CERTIFICATES
. " -d {$domain}", '', $stdout, $stderr);
// Unexpected error, usually 5XX, API limits, ...
if ($exit !== 0) {
throw new Exception('Failed to issue a certificate with message: ' . $stderr);
}
return [
'stdout' => $stdout,
'stderr' => $stderr
];
}
/**
* Read new renew date from certificate file generated by Let's Encrypt
*
* @param string $domain Domain which certificate was generated for
*
* @return int
*/
private function getRenewDate(string $domain): int
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
$certData = openssl_x509_parse(file_get_contents($certPath));
$validTo = $certData['validTo_time_t'] ?? 0;
$expiryInAdvance = (60 * 60 * 24 * 30); // 30 days
return $validTo - $expiryInAdvance;
}
/**
* Method to take files from Let's Encrypt, and put it into Traefik.
*
* @param string $domain Domain which certificate was generated for
* @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error
*
* @return void
*/
private function applyCertificateFiles(string $domain, array $letsEncryptData): void
{
// Prepare folder in storage for domain
$path = APP_STORAGE_CERTIFICATES . '/' . $domain;
if (!\is_readable($path)) {
if (!\mkdir($path, 0755, true)) {
throw new Exception('Failed to create path for certificate.');
}
}
// Move generated files from certbot into our storage
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) {
throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) {
throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) {
throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
if (!@\rename('/etc/letsencrypt/live/' . $domain . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) {
throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']);
}
$config = \implode(PHP_EOL, [
"tls:",
" certificates:",
" - certFile: /storage/certificates/{$domain}/fullchain.pem",
" keyFile: /storage/certificates/{$domain}/privkey.pem"
]);
// Save configuration into Traefik using our new cert files
if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) {
throw new Exception('Failed to save Traefik configuration.');
}
}
/**
* Method to make sure information about error is delivered to admnistrator.
*
* @param string $domain Domain that caused the error
* @param string $errorMessage Verbose error message
* @param int $attempt How many times it failed already
*
* @return void
*/
private function notifyError(string $domain, string $errorMessage, int $attempt): void
{
// Log error into console
Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage);
// Send mail to administratore mail
Resque::enqueue(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME, [
'from' => 'console',
'project' => 'console',
'name' => 'Appwrite Administrator',
'recipient' => App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'),
'url' => 'https://' . $domain,
'locale' => App::getEnv('_APP_LOCALE', 'en'),
'type' => MAIL_TYPE_CERTIFICATE,
'domain' => $domain,
'error' => $errorMessage,
'attempt' => $attempt
]);
}
/**
* Update all existing domain documents so they have relation to correct certificate document.
* This solved issues:
* - when adding a domain for which there is already a certificate
* - when renew creates new document? It might?
* - overall makes it more reliable
*
* @param string $certificateId ID of a new or updated certificate document
* @param string $domain Domain that is affected by new certificate
*
* @return void
*/
private function updateDomainDocuments(string $certificateId, string $domain): void
{
$domains = $this->dbForConsole->find('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain])
], 1000);
foreach ($domains as $domainDocument) {
$domainDocument->setAttribute('updated', \time());
$domainDocument->setAttribute('certificateId', $certificateId);
$this->dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument);
if ($domainDocument->getAttribute('projectId')) {
$this->dbForConsole->deleteCachedDocument('projects', $domainDocument->getAttribute('projectId'));
}
}
}
}
+74 -37
View File
@@ -1,15 +1,16 @@
<?php
use Appwrite\Event\Event;
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';
require_once __DIR__ . '/../init.php';
Console::title('Database V1 Worker');
Console::success(APP_NAME.' database worker v1 has started'."\n");
Console::success(APP_NAME . ' database worker v1 has started' . "\n");
class DatabaseV1 extends Worker
{
@@ -20,39 +21,38 @@ class DatabaseV1 extends Worker
public function run(): void
{
Authorization::disable();
$projectId = $this->args['projectId'] ?? '';
$type = $this->args['type'] ?? '';
$collection = $this->args['collection'] ?? [];
$collection = new Document($collection);
$document = $this->args['document'] ?? [];
$document = new Document($document);
if($collection->isEmpty()) {
$type = $this->args['type'];
$project = new Document($this->args['project']);
$collection = new Document($this->args['collection'] ?? []);
$document = new Document($this->args['document'] ?? []);
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
}
if($document->isEmpty()) {
if ($document->isEmpty()) {
throw new Exception('Missing document');
}
switch (strval($type)) {
case DATABASE_TYPE_CREATE_ATTRIBUTE:
$this->createAttribute($collection, $document, $projectId);
$this->createAttribute($collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_ATTRIBUTE:
$this->deleteAttribute($collection, $document, $projectId);
$this->deleteAttribute($collection, $document, $project->getId());
break;
case DATABASE_TYPE_CREATE_INDEX:
$this->createIndex($collection, $document, $projectId);
$this->createIndex($collection, $document, $project->getId());
break;
case DATABASE_TYPE_DELETE_INDEX:
$this->deleteIndex($collection, $document, $projectId);
$this->deleteIndex($collection, $document, $project->getId());
break;
default:
Console::error('No database operation for type: '.$type);
Console::error('No database operation for type: ' . $type);
break;
}
}
Authorization::reset();
}
@@ -71,7 +71,15 @@ class DatabaseV1 extends Worker
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$event = 'database.attributes.update';
$events = Event::generateEvents('collections.[collectionId].attributes.[attributeId].update', [
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
/**
* Fetch attribute from the database, since with Resque float values are loosing informations.
*/
$attribute = $dbForProject->getDocument('attributes', $attribute->getId());
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$type = $attribute->getAttribute('type', '');
@@ -86,7 +94,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('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'));
@@ -94,12 +102,17 @@ class DatabaseV1 extends Worker
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
} finally {
$target = Realtime::fromPayload($event, $attribute, $project);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $attribute->getArrayCopy(),
event: $event,
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
@@ -122,7 +135,10 @@ class DatabaseV1 extends Worker
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$event = 'database.attributes.delete';
$events = Event::generateEvents('collections.[collectionId].attributes.[attributeId].delete', [
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
@@ -135,7 +151,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('collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
@@ -143,12 +159,17 @@ class DatabaseV1 extends Worker
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
} finally {
$target = Realtime::fromPayload($event, $attribute, $project);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $attribute->getArrayCopy(),
event: $event,
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
@@ -173,7 +194,7 @@ class DatabaseV1 extends Worker
if ($found !== false) {
// If found, remove entry from attributes, lengths, and orders
// array_values wraps array_diff to reindex array keys
// array_values wraps array_diff to reindex array keys
// when found attribute is removed from array
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
$lengths = \array_values(\array_diff($lengths, [$lengths[$found]]));
@@ -191,7 +212,8 @@ class DatabaseV1 extends Worker
// Check if an index exists with the same attributes and orders
$exists = false;
foreach ($indexes as $existing) {
if ($existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
if (
$existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
) {
@@ -200,7 +222,7 @@ class DatabaseV1 extends Worker
}
}
if ($exists) { // Delete the duplicate if created, else update in db
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($collection, $index, $projectId);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
@@ -223,7 +245,10 @@ class DatabaseV1 extends Worker
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$event = 'database.indexes.update';
$events = Event::generateEvents('collections.[collectionId].indexes.[indexId].update', [
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$collectionId = $collection->getId();
$key = $index->getAttribute('key', '');
$type = $index->getAttribute('type', '');
@@ -233,7 +258,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('collection_' . $collection->getInternalId(), $key, $type, $attributes, $lengths, $orders)) {
throw new Exception('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
@@ -241,12 +266,17 @@ class DatabaseV1 extends Worker
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
} finally {
$target = Realtime::fromPayload($event, $index, $project);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $index,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $index->getArrayCopy(),
event: $event,
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
@@ -269,14 +299,16 @@ class DatabaseV1 extends Worker
$dbForConsole = $this->getConsoleDB();
$dbForProject = $this->getProjectDB($projectId);
$collectionId = $collection->getId();
$events = Event::generateEvents('collections.[collectionId].indexes.[indexId].delete', [
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
$event = 'database.indexes.delete';
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if($status !== 'failed' && !$dbForProject->deleteIndex('collection_' . $collection->getInternalId(), $key)) {
if ($status !== 'failed' && !$dbForProject->deleteIndex('collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete index');
}
$dbForProject->deleteDocument('indexes', $index->getId());
@@ -284,12 +316,17 @@ class DatabaseV1 extends Worker
Console::error($th->getMessage());
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
} finally {
$target = Realtime::fromPayload($event, $index, $project);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $index,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $index->getArrayCopy(),
event: $event,
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
@@ -299,6 +336,6 @@ class DatabaseV1 extends Worker
);
}
$dbForProject->deleteCachedDocument('collections', $collectionId);
$dbForProject->deleteCachedDocument('collections', $collection->getId());
}
}
+64 -53
View File
@@ -8,9 +8,6 @@ use Utopia\Database\Validator\Authorization;
use Appwrite\Resque\Worker;
use Executor\Executor;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\DOSpaces;
use Utopia\Storage\Storage;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\CLI\Console;
@@ -19,6 +16,7 @@ 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");
@@ -30,7 +28,8 @@ class DeletesV1 extends Worker
*/
protected $consoleDB = null;
public function getName(): string {
public function getName(): string
{
return "deletes";
}
@@ -40,7 +39,7 @@ class DeletesV1 extends Worker
public function run(): void
{
$projectId = $this->args['projectId'] ?? '';
$project = new Document($this->args['project'] ?? []);
$type = $this->args['type'] ?? '';
switch (strval($type)) {
@@ -49,25 +48,25 @@ class DeletesV1 extends Worker
switch ($document->getCollection()) {
case DELETE_TYPE_COLLECTIONS:
$this->deleteCollection($document, $projectId);
$this->deleteCollection($document, $project->getId());
break;
case DELETE_TYPE_PROJECTS:
$this->deleteProject($document);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($document, $projectId);
$this->deleteFunction($document, $project->getId());
break;
case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($document, $projectId);
$this->deleteDeployment($document, $project->getId());
break;
case DELETE_TYPE_USERS:
$this->deleteUser($document, $projectId);
$this->deleteUser($document, $project->getId());
break;
case DELETE_TYPE_TEAMS:
$this->deleteMemberships($document, $projectId);
$this->deleteMemberships($document, $project->getId());
break;
case DELETE_TYPE_BUCKETS:
$this->deleteBucket($document, $projectId);
$this->deleteBucket($document, $project->getId());
break;
default:
Console::error('No lazy delete operation available for document of type: ' . $document->getCollection());
@@ -80,15 +79,15 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_AUDIT:
$timestamp = $this->args['timestamp'] ?? 0;
$document = new Document($this->args['document'] ?? []);
$timestamp = $payload['timestamp'] ?? 0;
$document = new Document($payload['document'] ?? []);
if (!empty($timestamp)) {
$this->deleteAuditLogs($this->args['timestamp']);
}
if (!$document->isEmpty()) {
$this->deleteAuditLogsByResource('document/' . $document->getId(), $projectId);
$this->deleteAuditLogsByResource('document/' . $document->getId(), $project->getId());
}
break;
@@ -201,21 +200,14 @@ class DeletesV1 extends Worker
*/
protected function deleteUser(Document $document, string $projectId): void
{
/**
* DO NOT DELETE THE USER RECORD ITSELF.
* WE RETAIN THE USER RECORD TO RESERVE THE USER ID AND ENSURE THAT THE USER ID IS NOT REUSED.
*/
$userId = $document->getId();
$user = $this->getProjectDB($projectId)->getDocument('users', $userId);
// Delete all sessions of this user from the sessions table and update the sessions field of the user record
$this->deleteByGroup('sessions', [
new Query('userId', Query::TYPE_EQUAL, [$userId])
], $this->getProjectDB($projectId));
$user->setAttribute('sessions', []);
$updated = $this->getProjectDB($projectId)->updateDocument('users', $userId, $user);
$this->getProjectDB($projectId)->deleteCachedDocument('users', $userId);
// Delete Memberships and decrement team membership counts
$this->deleteByGroup('memberships', [
@@ -226,12 +218,22 @@ class DeletesV1 extends Worker
$teamId = $document->getAttribute('teamId');
$team = $this->getProjectDB($projectId)->getDocument('teams', $teamId);
if (!$team->isEmpty()) {
$team = $this->getProjectDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [
'total' => \max($team->getAttribute('total', 0) - 1, 0), // Ensure that total >= 0
])));
$team = $this
->getProjectDB($projectId)
->updateDocument(
'teams',
$teamId,
// Ensure that total >= 0
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0))
);
}
}
});
// Delete tokens
$this->deleteByGroup('tokens', [
new Query('userId', Query::TYPE_EQUAL, [$userId])
], $this->getProjectDB($projectId));
}
/**
@@ -344,7 +346,7 @@ class DeletesV1 extends Worker
*/
Console::info("Deleting builds for function " . $functionId);
$storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId);
foreach ($deploymentIds as $deploymentId) {
foreach ($deploymentIds as $deploymentId) {
$this->deleteByGroup('builds', [
new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId])
], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId) {
@@ -356,9 +358,9 @@ class DeletesV1 extends Worker
});
}
/**
/**
* Delete Executions
*/
*/
Console::info("Deleting executions for function " . $functionId);
$this->deleteByGroup('executions', [
new Query('functionId', Query::TYPE_EQUAL, [$functionId])
@@ -376,7 +378,6 @@ class DeletesV1 extends Worker
Console::error($th->getMessage());
}
}
}
/**
@@ -470,7 +471,7 @@ class DeletesV1 extends Worker
$chunk++;
/** @var string[] $projectIds */
$projectIds = array_map(fn(Document $project) => $project->getId(), $projects);
$projectIds = array_map(fn (Document $project) => $project->getId(), $projects);
$sum = count($projects);
@@ -522,15 +523,44 @@ class DeletesV1 extends Worker
}
/**
* @param Document $document certificates document
* @param Document $document certificates document
*/
protected function deleteCertificates(Document $document): void
{
$consoleDB = $this->getConsoleDB();
// If domain has certificate generated
if (isset($document['certificateId'])) {
$domainUsingCertificate = $consoleDB->findOne('domains', [
new Query('certificateId', Query::TYPE_EQUAL, [$document['certificateId']])
]);
if (!$domainUsingCertificate) {
$mainDomain = App::getEnv('_APP_DOMAIN_TARGET', '');
if ($mainDomain === $document->getAttribute('domain')) {
$domainUsingCertificate = $mainDomain;
}
}
// If certificate is still used by some domain, mark we can't delete.
// Current domain should not be found, because we only have copy. Original domain is already deleted from database.
if ($domainUsingCertificate) {
Console::warning("Skipping certificate deletion, because a domain is still using it.");
return;
}
}
$domain = $document->getAttribute('domain');
$directory = APP_STORAGE_CERTIFICATES . '/' . $domain;
$checkTraversal = realpath($directory) === $directory;
if ($domain && $checkTraversal && is_dir($directory)) {
// Delete certificate document, so Appwrite is aware of change
if (isset($document['certificateId'])) {
$consoleDB->deleteDocument('certificates', $document['certificateId']);
}
// Delete files, so Traefik is aware of change
array_map('unlink', glob($directory . '/*.*'));
rmdir($directory);
Console::info("Deleted certificate files for {$domain}");
@@ -544,27 +574,8 @@ class DeletesV1 extends Worker
$dbForProject = $this->getProjectDB($projectId);
$dbForProject->deleteCollection('bucket_' . $document->getInternalId());
$device = new Local(APP_STORAGE_UPLOADS.'/app-'.$projectId);
switch (App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL)) {
case Storage::DEVICE_S3:
$s3AccessKey = App::getEnv('_APP_STORAGE_S3_ACCESS_KEY', '');
$s3SecretKey = App::getEnv('_APP_STORAGE_S3_SECRET', '');
$s3Region = App::getEnv('_APP_STORAGE_S3_REGION', '');
$s3Bucket = App::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
$device = new S3(APP_STORAGE_UPLOADS . '/app-' . $projectId, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
break;
case Storage::DEVICE_DO_SPACES:
$doSpacesAccessKey = App::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = App::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
$doSpacesRegion = App::getEnv('_APP_STORAGE_DO_SPACES_REGION', '');
$doSpacesBucket = App::getEnv('_APP_STORAGE_DO_SPACES_BUCKET', '');
$doSpacesAcl = 'private';
$device = new DOSpaces(APP_STORAGE_UPLOADS . '/app-' . $projectId, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
break;
}
$device = $this->getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
$device->deletePath($document->getId());
}
}
+159 -150
View File
@@ -1,13 +1,13 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Response\Model\Execution;
use Cron\CronExpression;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -15,23 +15,19 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Functions V1 Worker');
Console::success(APP_NAME . ' functions worker v1 has started');
class FunctionsV1 extends Worker
{
/**
* @var Executor
*/
private $executor = null;
private ?Executor $executor = null;
public array $args = [];
public array $allowed = [];
public function getName(): string {
public function getName(): string
{
return "functions";
}
@@ -42,65 +38,86 @@ class FunctionsV1 extends Worker
public function run(): void
{
$projectId = $this->args['projectId'] ?? '';
$functionId = $this->args['functionId'] ?? '';
$webhooks = $this->args['webhooks'] ?? [];
$executionId = $this->args['executionId'] ?? '';
$trigger = $this->args['trigger'] ?? '';
$event = $this->args['event'] ?? '';
$scheduleOriginal = $this->args['scheduleOriginal'] ?? '';
$eventData = (!empty($this->args['eventData'])) ? json_encode($this->args['eventData']) : '';
$data = $this->args['data'] ?? '';
$userId = $this->args['userId'] ?? '';
$jwt = $this->args['jwt'] ?? '';
$type = $this->args['type'] ?? '';
$events = $this->args['events'] ?? [];
$project = new Document($this->args['project'] ?? []);
$user = new Document($this->args['user'] ?? []);
$payload = json_encode($this->args['payload'] ?? []);
$database = $this->getProjectDB($projectId);
$database = $this->getProjectDB($project->getId());
switch ($trigger) {
case 'event':
$limit = 30;
$sum = 30;
$offset = 0;
$functions = [];
/** @var Document[] $functions */
/**
* Handle Event execution.
*/
if (!empty($events)) {
$limit = 30;
$sum = 30;
$offset = 0;
$functions = [];
/** @var Document[] $functions */
while ($sum >= $limit) {
$functions = Authorization::skip(fn() => $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]));
$sum = \count($functions);
$offset = $offset + $limit;
while ($sum >= $limit) {
$functions = Authorization::skip(fn () => $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]));
$sum = \count($functions);
$offset = $offset + $limit;
Console::log('Fetched ' . $sum . ' functions...');
Console::log('Fetched ' . $sum . ' functions...');
foreach ($functions as $function) {
$events = $function->getAttribute('events', []);
if (!\in_array($event, $events)) {
continue;
}
Console::success('Iterating function: ' . $function->getAttribute('name'));
$this->execute(
projectId: $projectId,
function: $function,
dbForProject: $database,
executionId: $executionId,
webhooks: $webhooks,
trigger: $trigger,
event: $event,
eventData: $eventData,
data: $data,
userId: $userId,
jwt: $jwt
);
Console::success('Triggered function: ' . $event);
foreach ($functions as $function) {
if (!array_intersect($events, $function->getAttribute('events', []))) {
continue;
}
Console::success('Iterating function: ' . $function->getAttribute('name'));
$this->execute(
project: $project,
function: $function,
dbForProject: $database,
trigger: 'event',
// Pass first, most verbose event pattern
event: $events[0],
eventData: $payload,
user: $user
);
Console::success('Triggered function: ' . $events[0]);
}
}
return;
}
/**
* Handle Schedule and HTTP execution.
*/
$user = new Document($this->args['user'] ?? []);
$project = new Document($this->args['project'] ?? []);
$execution = new Document($this->args['execution'] ?? []);
$function = new Document($this->args['function'] ?? []);
switch ($type) {
case 'http':
$jwt = $this->args['jwt'] ?? '';
$data = $this->args['data'] ?? '';
$function = Authorization::skip(fn () => $database->getDocument('functions', $execution->getAttribute('functionId')));
$this->execute(
project: $project,
function: $function,
dbForProject: $database,
executionId: $execution->getId(),
trigger: 'http',
data: $data,
user: $user,
jwt: $jwt
);
break;
case 'schedule':
$scheduleOriginal = $execution->getAttribute('scheduleOriginal', '');
/*
* 1. Get Original Task
* 2. Check for updates
@@ -115,10 +132,10 @@ class FunctionsV1 extends Worker
*/
// Reschedule
$function = Authorization::skip(fn() => $database->getDocument('functions', $functionId));
$function = Authorization::skip(fn () => $database->getDocument('functions', $function->getId()));
if (empty($function->getId())) {
throw new Exception('Function not found ('.$functionId.')');
throw new Exception('Function not found (' . $function->getId() . ')');
}
if ($scheduleOriginal && $scheduleOriginal !== $function->getAttribute('schedule')) { // Schedule has changed from previous run, ignore this run.
@@ -132,61 +149,31 @@ class FunctionsV1 extends Worker
->setAttribute('scheduleNext', $next)
->setAttribute('schedulePrevious', \time());
$function = Authorization::skip(function() use ($database, $function, $next, $functionId) {
$function = $database->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'scheduleNext' => (int)$next,
])));
if ($function === false) {
throw new Exception('Function update failed (' . $functionId . ')');
}
return $function;
});
$function = Authorization::skip(fn () => $database->updateDocument(
'functions',
$function->getId(),
$function->setAttribute('scheduleNext', (int) $next)
));
ResqueScheduler::enqueueAt($next, Event::FUNCTIONS_QUEUE_NAME, Event::FUNCTIONS_CLASS_NAME, [
'projectId' => $projectId,
'webhooks' => $webhooks,
'functionId' => $function->getId(),
'userId' => $userId,
'executionId' => null,
'trigger' => 'schedule',
'scheduleOriginal' => $function->getAttribute('schedule', ''),
]); // Async task reschedule
$this->execute(
projectId: $projectId,
function: $function,
dbForProject: $database,
executionId: $executionId,
webhooks: $webhooks,
trigger: $trigger,
event: $event,
eventData: $eventData,
data: $data,
userId: $userId,
jwt: $jwt
);
break;
case 'http':
$function = Authorization::skip(fn() => $database->getDocument('functions', $functionId));
if (empty($function->getId())) {
throw new Exception('Function not found ('.$functionId.')');
if ($function === false) {
throw new Exception('Function update failed.');
}
$reschedule = new Func();
$reschedule
->setFunction($function)
->setType('schedule')
->setUser($user)
->setProject($project);
// Async task reschedule
$reschedule->schedule($next);
$this->execute(
projectId: $projectId,
project: $project,
function: $function,
dbForProject: $database,
executionId: $executionId,
webhooks: $webhooks,
trigger: $trigger,
event: $event,
eventData: $eventData,
data: $data,
userId: $userId,
jwt: $jwt
trigger: 'schedule'
);
break;
@@ -194,24 +181,24 @@ class FunctionsV1 extends Worker
}
private function execute(
string $projectId,
Document $project,
Document $function,
Database $dbForProject,
string $executionId,
array $webhooks,
string $trigger,
string $event,
string $eventData,
string $data,
string $userId,
string $jwt
string $executionId = null,
string $event = null,
string $eventData = null,
string $data = null,
?Document $user = null,
string $jwt = null
) {
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deployment', '');
/** Check if deployment exists */
$deployment = Authorization::skip(fn() => $dbForProject->getDocument('deployments', $deploymentId));
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId));
if ($deployment->getAttribute('resourceId') !== $functionId) {
throw new Exception('Deployment not found. Create deployment before trying to execute a function', 404);
@@ -222,7 +209,7 @@ class FunctionsV1 extends Worker
}
/** Check if build has exists */
$build = Authorization::skip(fn() => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new Exception('Build not found', 404);
}
@@ -233,20 +220,21 @@ class FunctionsV1 extends Worker
/** Check if runtime is supported */
$runtimes = Config::getParam('runtimes', []);
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
if (\is_null($runtime)) {
if (!\array_key_exists($function->getAttribute('runtime'), $runtimes)) {
throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported', 400);
}
$runtime = $runtimes[$function->getAttribute('runtime')];
/** Create execution or update execution status */
$execution = Authorization::skip(function() use ($dbForProject, &$executionId, $functionId, $deploymentId, $trigger, $userId) {
$execution = $dbForProject->getDocument('executions', $executionId);
$execution = Authorization::skip(function () use ($dbForProject, &$executionId, $functionId, $deploymentId, $trigger, $user) {
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
$executionId = $dbForProject->getId();
$execution = $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => $userId ? ['user:' . $userId] : [],
'$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()],
'$write' => [],
'dateCreated' => time(),
'functionId' => $functionId,
@@ -254,18 +242,19 @@ class FunctionsV1 extends Worker
'trigger' => $trigger,
'status' => 'waiting',
'statusCode' => 0,
'stdout' => '',
'response' => '',
'stderr' => '',
'time' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
]));
if ($execution->isEmpty()) {
throw new Exception('Failed to create or read execution');
}
}
$execution->setAttribute('status', 'processing');
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
return $execution;
});
@@ -280,8 +269,8 @@ class FunctionsV1 extends Worker
'APPWRITE_FUNCTION_EVENT' => $event,
'APPWRITE_FUNCTION_EVENT_DATA' => $eventData,
'APPWRITE_FUNCTION_DATA' => $data,
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
'APPWRITE_FUNCTION_USER_ID' => $userId,
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_USER_ID' => $user->getId(),
'APPWRITE_FUNCTION_JWT' => $jwt,
];
$vars = \array_merge($function->getAttribute('vars', []), $vars);
@@ -289,56 +278,77 @@ class FunctionsV1 extends Worker
/** Execute function */
try {
$executionResponse = $this->executor->createExecution(
projectId: $projectId,
projectId: $project->getId(),
deploymentId: $deploymentId,
path: $build->getAttribute('outputPath', ''),
vars: $vars,
entrypoint: $deployment->getAttribute('entrypoint', ''),
data: $vars['APPWRITE_FUNCTION_DATA'],
data: $vars['APPWRITE_FUNCTION_DATA'] ?? '',
runtime: $function->getAttribute('runtime', ''),
timeout: $function->getAttribute('timeout', 0),
baseImage: $runtime['image']
);
/** Update execution status */
$execution->setAttribute('status', $executionResponse['status']);
$execution->setAttribute('statusCode', $executionResponse['statusCode']);
$execution->setAttribute('stdout', $executionResponse['stdout']);
$execution->setAttribute('stderr', $executionResponse['stderr']);
$execution->setAttribute('time', $executionResponse['time']);
$execution
->setAttribute('status', $executionResponse['status'])
->setAttribute('statusCode', $executionResponse['statusCode'])
->setAttribute('response', $executionResponse['response'])
->setAttribute('stderr', $executionResponse['stderr'])
->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
$execution->setAttribute('status', 'failed');
$execution->setAttribute('statusCode', $th->getCode());
$execution->setAttribute('stderr', $th->getMessage());
$endtime = \microtime(true);
$time = $endtime - $execution->getAttribute('dateCreated');
$execution
->setAttribute('time', $time)
->setAttribute('status', 'failed')
->setAttribute('statusCode', $th->getCode())
->setAttribute('stderr', $th->getMessage());
Console::error($th->getMessage());
}
$execution = Authorization::skip(fn() => $dbForProject->updateDocument('executions', $executionId, $execution));
$execution = Authorization::skip(fn () => $dbForProject->updateDocument('executions', $executionId, $execution));
/** @var Document $execution */
/** Trigger Webhook */
$executionModel = new Execution();
$executionUpdate = new Event(Event::WEBHOOK_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME);
$executionUpdate
->setParam('projectId', $projectId)
->setParam('userId', $userId)
->setParam('webhooks', $webhooks)
->setParam('event', 'functions.executions.update')
->setParam('eventData', $execution->getArrayCopy(array_keys($executionModel->getRules())));
$executionUpdate->trigger();
->setProject($project)
->setUser($user)
->setEvent('functions.[functionId].executions.[executionId].update')
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setPayload($execution->getArrayCopy(array_keys($executionModel->getRules())))
->trigger();
/** Trigger Functions */
$executionUpdate
->setClass(Event::FUNCTIONS_CLASS_NAME)
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
->trigger();
/** Trigger realtime event */
$target = Realtime::fromPayload('functions.executions.update', $execution);
$allEvents = Event::generateEvents('functions.[functionId].executions.[executionId].update', [
'functionId' => $function->getId(),
'executionId' => $execution->getId()
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $execution
);
Realtime::send(
projectId: 'console',
payload: $execution->getArrayCopy(),
event: 'functions.executions.update',
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $projectId,
projectId: $project->getId(),
payload: $execution->getArrayCopy(),
event: 'functions.executions.update',
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
@@ -349,7 +359,7 @@ class FunctionsV1 extends Worker
$statsd = $register->get('statsd');
$usage = new Stats($statsd);
$usage
->setParam('projectId', $projectId)
->setParam('projectId', $project->getId())
->setParam('functionId', $function->getId())
->setParam('functionExecution', 1)
->setParam('functionStatus', $execution->getAttribute('status', ''))
@@ -357,7 +367,6 @@ class FunctionsV1 extends Worker
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();
$usage->submit();
}
}
+30 -12
View File
@@ -4,6 +4,7 @@ use Appwrite\Resque\Worker;
use Appwrite\Template\Template;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Locale\Locale;
require_once __DIR__ . '/../init.php';
@@ -13,7 +14,8 @@ Console::success(APP_NAME . ' mails worker v1 has started' . "\n");
class MailsV1 extends Worker
{
public function getName(): string {
public function getName(): string
{
return "mails";
}
@@ -30,26 +32,40 @@ class MailsV1 extends Worker
return;
}
$project = new Document($this->args['project']);
$user = new Document($this->args['user'] ?? []);
$team = new Document($this->args['team'] ?? []);
$recipient = $this->args['recipient'];
$name = $this->args['name'];
$url = $this->args['url'];
$project = $this->args['project'];
$name = $this->args['name'];
$type = $this->args['type'];
$prefix = $this->getPrefix($type);
$locale = new Locale($this->args['locale']);
$projectName = $project->getAttribute('name', '[APP-NAME]');
if (!$this->doesLocaleExist($locale, $prefix)) {
$locale->setDefault('en');
}
$from = $this->args['from'] === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $project);
$from = $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl');
$subject = '';
switch ($type) {
case MAIL_TYPE_CERTIFICATE:
$domain = $this->args['domain'];
$error = $this->args['error'];
$attempt = $this->args['attempt'];
$subject = \sprintf($locale->getText("$prefix.subject"), $domain);
$body->setParam('{{domain}}', $domain);
$body->setParam('{{error}}', $error);
$body->setParam('{{attempt}}', $attempt);
break;
case MAIL_TYPE_INVITATION:
$subject = \sprintf($locale->getText("$prefix.subject"), $this->args['team'], $project);
$body->setParam('{{owner}}', $this->args['owner']);
$body->setParam('{{team}}', $this->args['team']);
$subject = \sprintf($locale->getText("$prefix.subject"), $team->getAttribute('name'), $projectName);
$body->setParam('{{owner}}', $user->getAttribute('name'));
$body->setParam('{{team}}', $team->getAttribute('name'));
break;
case MAIL_TYPE_RECOVERY:
case MAIL_TYPE_VERIFICATION:
@@ -69,7 +85,7 @@ class MailsV1 extends Worker
->setParam('{{footer}}', $locale->getText("$prefix.footer"))
->setParam('{{thanks}}', $locale->getText("$prefix.thanks"))
->setParam('{{signature}}', $locale->getText("$prefix.signature"))
->setParam('{{project}}', $project)
->setParam('{{project}}', $projectName)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
@@ -116,9 +132,9 @@ class MailsV1 extends Worker
/**
* Returns a prefix from a mail type
*
*
* @param $type
*
*
* @return string
*/
protected function getPrefix(string $type): string
@@ -126,6 +142,8 @@ class MailsV1 extends Worker
switch ($type) {
case MAIL_TYPE_RECOVERY:
return 'emails.recovery';
case MAIL_TYPE_CERTIFICATE:
return 'emails.certificate';
case MAIL_TYPE_INVITATION:
return 'emails.invitation';
case MAIL_TYPE_VERIFICATION:
@@ -139,10 +157,10 @@ class MailsV1 extends Worker
/**
* Returns true if all the required terms in a locale exist. False otherwise
*
*
* @param $locale
* @param $prefix
*
*
* @return bool
*/
protected function doesLocaleExist(Locale $locale, string $prefix): bool
+66 -65
View File
@@ -3,15 +3,19 @@
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
require_once __DIR__.'/../init.php';
require_once __DIR__ . '/../init.php';
Console::title('Webhooks V1 Worker');
Console::success(APP_NAME . ' webhooks worker v1 has started');
class WebhooksV1 extends Worker
{
public function getName(): string {
protected array $errors = [];
public function getName(): string
{
return "webhooks";
}
@@ -21,77 +25,74 @@ class WebhooksV1 extends Worker
public function run(): void
{
$errors = [];
$events = $this->args['events'];
$payload = json_encode($this->args['payload']);
$project = new Document($this->args['project']);
$user = new Document($this->args['user'] ?? []);
// Event
$projectId = $this->args['projectId'] ?? '';
$webhooks = $this->args['webhooks'] ?? [];
$userId = $this->args['userId'] ?? '';
$event = $this->args['event'] ?? '';
$eventData = \json_encode($this->args['eventData']);
foreach ($webhooks as $webhook) {
if (!(isset($webhook['events']) && \is_array($webhook['events']) && \in_array($event, $webhook['events']))) {
continue;
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
$this->execute($events, $payload, $webhook, $user, $project);
}
$id = $webhook['$id'] ?? '';
$name = $webhook['name'] ?? '';
$signature = $webhook['signature'] ?? 'not-yet-implemented';
$url = $webhook['url'] ?? '';
$security = (bool) ($webhook['security'] ?? true);
$httpUser = $webhook['httpUser'] ?? null;
$httpPass = $webhook['httpPass'] ?? null;
$ch = \curl_init($url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $eventData);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
));
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . \strlen($eventData),
'X-' . APP_NAME . '-Webhook-Id: ' . $id,
'X-' . APP_NAME . '-Webhook-Event: ' . $event,
'X-' . APP_NAME . '-Webhook-Name: ' . $name,
'X-' . APP_NAME . '-Webhook-User-Id: ' . $userId,
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $projectId,
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
]
);
if (!$security) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
if (!empty($httpUser) && !empty($httpPass)) {
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
}
if (false === \curl_exec($ch)) {
$errors[] = \curl_error($ch) . ' in event ' . $event . ' for webhook ' . $name;
}
\curl_close($ch);
}
if (!empty($errors)) {
throw new Exception(\implode(" / \n\n", $errors));
if (!empty($this->errors)) {
throw new Exception(\implode(" / \n\n", $this->errors));
}
}
protected function execute(array $events, string $payload, Document $webhook, Document $user, Document $project): void
{
$url = \rawurldecode($webhook->getAttribute('url'));
$signatureKey = $webhook->getAttribute('signatureKey');
$signature = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$httpUser = $webhook->getAttribute('httpUser');
$httpPass = $webhook->getAttribute('httpPass');
$ch = \curl_init($webhook->getAttribute('url'));
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
App::getEnv('_APP_VERSION', 'UNKNOWN'),
App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
));
\curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: ' . \strlen($payload),
'X-' . APP_NAME . '-Webhook-Id: ' . $webhook->getId(),
'X-' . APP_NAME . '-Webhook-Events: ' . implode(',', $events),
'X-' . APP_NAME . '-Webhook-Name: ' . $webhook->getAttribute('name', ''),
'X-' . APP_NAME . '-Webhook-User-Id: ' . $user->getId(),
'X-' . APP_NAME . '-Webhook-Project-Id: ' . $project->getId(),
'X-' . APP_NAME . '-Webhook-Signature: ' . $signature,
]
);
if (!$webhook->getAttribute('security', true)) {
\curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
\curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
}
if (!empty($httpUser) && !empty($httpPass)) {
\curl_setopt($ch, CURLOPT_USERPWD, "$httpUser:$httpPass");
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
}
if (false === \curl_exec($ch)) {
$this->errors[] = \curl_error($ch) . ' in events ' . implode(', ', $events) . ' for webhook ' . $webhook->getAttribute('name');
}
\curl_close($ch);
}
public function shutdown(): void
{
$this->errors = [];
}
}
+9 -9
View File
@@ -36,7 +36,7 @@
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.7.*",
"appwrite/php-runtimes": "0.9.*",
"utopia-php/framework": "0.19.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.7.*",
@@ -45,24 +45,25 @@
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.12.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.15.*",
"utopia-php/database": "0.17.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "dev-feat-allow-querying",
"utopia-php/preloader": "0.2.*",
"utopia-php/domains": "1.1.*",
"utopia-php/swoole": "0.3.*",
"utopia-php/storage": "0.7.*",
"utopia-php/storage": "0.9.*",
"utopia-php/websocket": "0.1.0",
"utopia-php/image": "0.5.*",
"utopia-php/orchestration": "0.4.*",
"resque/php-resque": "1.3.6",
"matomo/device-detector": "5.0.4",
"matomo/device-detector": "6.0.0",
"dragonmantank/cron-expression": "3.3.1",
"influxdb/influxdb-php": "1.15.2",
"phpmailer/phpmailer": "6.6.0",
"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": [
@@ -72,11 +73,10 @@
}
],
"require-dev": {
"appwrite/sdk-generator": "0.18.3",
"phpunit/phpunit": "9.5.10",
"swoole/ide-helper": "4.8.5",
"textalk/websocket": "1.5.5",
"vimeo/psalm": "4.13.1"
"appwrite/sdk-generator": "0.18.8",
"phpunit/phpunit": "9.5.20",
"swoole/ide-helper": "4.8.9",
"textalk/websocket": "1.5.7"
},
"provide": {
"ext-phpiredis": "*"
Generated
+183 -961
View File
File diff suppressed because it is too large Load Diff
+37 -29
View File
@@ -10,11 +10,35 @@ x-logging: &x-logging
max-file: '5'
max-size: '10m'
x-env-storage: &x-env-storage |-
_APP_STORAGE_DEVICE
_APP_STORAGE_S3_ACCESS_KEY
_APP_STORAGE_S3_SECRET
_APP_STORAGE_S3_REGION
_APP_STORAGE_S3_BUCKET
_APP_STORAGE_DO_SPACES_ACCESS_KEY
_APP_STORAGE_DO_SPACES_SECRET
_APP_STORAGE_DO_SPACES_REGION
_APP_STORAGE_DO_SPACES_BUCKET
_APP_STORAGE_BACKBLAZE_ACCESS_KEY
_APP_STORAGE_BACKBLAZE_SECRET
_APP_STORAGE_BACKBLAZE_REGION
_APP_STORAGE_BACKBLAZE_BUCKET
_APP_STORAGE_DO_SPACES_BUCKET
_APP_STORAGE_LINODE_ACCESS_KEY
_APP_STORAGE_LINODE_SECRET
_APP_STORAGE_LINODE_REGION
_APP_STORAGE_LINODE_BUCKET
_APP_STORAGE_WASABI_ACCESS_KEY
_APP_STORAGE_WASABI_SECRET
_APP_STORAGE_WASABI_REGION
_APP_STORAGE_WASABI_BUCKET
version: '3'
services:
traefik:
image: traefik:2.5
image: traefik:2.7
<<: *x-logging
container_name: appwrite-traefik
command:
@@ -77,7 +101,6 @@ services:
- appwrite-certificates:/storage/certificates:rw
- appwrite-functions:/storage/functions:rw
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./psalm.xml:/usr/src/code/psalm.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
- ./vendor:/usr/src/code/vendor
@@ -129,18 +152,11 @@ services:
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_STORAGE_LIMIT
- _APP_STORAGE_PREVIEW_LIMIT
- _APP_STORAGE_ANTIVIRUS
- _APP_STORAGE_ANTIVIRUS_HOST
- _APP_STORAGE_ANTIVIRUS_PORT
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- *x-env-storage
- _APP_FUNCTIONS_SIZE_LIMIT
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_BUILD_TIMEOUT
@@ -299,15 +315,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- *x-env-storage
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_EXECUTOR_SECRET
@@ -393,6 +401,7 @@ services:
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
- _APP_REDIS_HOST
@@ -483,15 +492,7 @@ services:
- OPEN_RUNTIMES_NETWORK
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
- _APP_STORAGE_DO_SPACES_BUCKET
- *x-env-storage
- DOCKERHUB_PULL_USERNAME
- DOCKERHUB_PULL_PASSWORD
@@ -542,11 +543,18 @@ services:
- redis
environment:
- _APP_ENV
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
@@ -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());
}
}
});
}
}

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