Merge branch 'feat-release-preps' of https://github.com/appwrite/appwrite into feat-refactor-tasks

This commit is contained in:
Damodar Lohani 2022-09-01 12:00:13 +00:00
commit 53c784a6fb
2347 changed files with 68164 additions and 17902 deletions

8
.env
View file

@ -56,8 +56,8 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_PHONE_PROVIDER=phone://mock
_APP_PHONE_FROM=+123456789
_APP_SMS_PROVIDER=sms://mock
_APP_SMS_FROM=+123456789
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000
@ -72,10 +72,12 @@ OPEN_RUNTIMES_NETWORK=appwrite_runtimes
_APP_EXECUTOR_SECRET=your-secret-key
_APP_EXECUTOR_HOST=http://appwrite-executor/v1
_APP_MAINTENANCE_INTERVAL=86400
_APP_MAINTENANCE_RETENTION_CACHE=2592000
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
_APP_USAGE_AGGREGATION_INTERVAL=30
_APP_USAGE_TIMESERIES_INTERVAL=2
_APP_USAGE_DATABASE_INTERVAL=15
_APP_USAGE_STATS=enabled
_APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=

View file

@ -37,6 +37,7 @@ body:
label: "🎲 Appwrite version"
description: "What version of Appwrite are you running?"
options:
- Version 1.0.0-RC1
- Version 0.15.x
- Version 0.14.x
- Version 0.13.x

View file

@ -142,7 +142,7 @@ Learn more at our [Technology Stack](#technology-stack) section.
##### Security
- [Appwrite Auth and ACL](https://github.com/appwrite/appwrite/blob/0.7.x/docs/specs/authentication.drawio.svg)
- [Appwrite Auth and ACL](https://github.com/appwrite/appwrite/blob/master/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.)
- [Hashing](https://searchsqlserver.techtarget.com/definition/hashing#:~:text=Hashing%20is%20the%20transformation%20of,it%20using%20the%20original%20value.)

View file

@ -34,7 +34,8 @@ ENV PHP_REDIS_VERSION=5.3.7 \
PHP_SWOOLE_VERSION=v4.8.10 \
PHP_IMAGICK_VERSION=3.7.0 \
PHP_YAML_VERSION=2.2.2 \
PHP_MAXMINDDB_VERSION=v1.11.0
PHP_MAXMINDDB_VERSION=v1.11.0 \
PHP_ZSTD_VERSION="4504e4186e79b197cfcb75d4d09aa47ef7d92fe9 "
RUN \
apk add --no-cache --virtual .deps \
@ -50,7 +51,8 @@ RUN \
yaml-dev \
imagemagick \
imagemagick-dev \
libmaxminddb-dev
libmaxminddb-dev \
zstd-dev
RUN docker-php-ext-install sockets
@ -123,6 +125,43 @@ RUN \
./configure && \
make && make install
# Zstd Compression
FROM compile as zstd
RUN git clone --recursive -n https://github.com/kjdev/php-ext-zstd.git \
&& cd php-ext-zstd \
&& git checkout $PHP_ZSTD_VERSION \
&& phpize \
&& ./configure --with-libzstd \
&& make && make install
# Rust Extensions Compile Image
FROM php:8.0.18-cli as rust_compile
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH=/root/.cargo/bin:$PATH
RUN apt-get update && apt-get install musl-tools build-essential clang-11 git -y
RUN rustup target add $(uname -m)-unknown-linux-musl
# Install ZigBuild for easier cross-compilation
RUN curl https://ziglang.org/builds/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38.tar.xz --output /tmp/zig.tar.xz
RUN tar -xf /tmp/zig.tar.xz -C /tmp/ && cp -r /tmp/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38 /tmp/zig/
ENV PATH=/tmp/zig:$PATH
RUN cargo install cargo-zigbuild
ENV RUSTFLAGS="-C target-feature=-crt-static"
FROM rust_compile as scrypt
WORKDIR /usr/local/lib/php/extensions/
RUN \
git clone --depth 1 https://github.com/appwrite/php-scrypt.git && \
cd php-scrypt && \
cargo zigbuild --workspace --all-targets --target $(uname -m)-unknown-linux-musl --release && \
mv target/$(uname -m)-unknown-linux-musl/release/libphp_scrypt.so target/libphp_scrypt.so
FROM php:8.0.18-cli-alpine3.15 as final
LABEL maintainer="team@appwrite.io"
@ -193,8 +232,8 @@ ENV _APP_SERVER=swoole \
_APP_SMTP_SECURE= \
_APP_SMTP_USERNAME= \
_APP_SMTP_PASSWORD= \
_APP_PHONE_PROVIDER= \
_APP_PHONE_FROM= \
_APP_SMS_PROVIDER= \
_APP_SMS_FROM= \
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
_APP_FUNCTIONS_TIMEOUT=900 \
_APP_FUNCTIONS_CONTAINERS=10 \
@ -207,6 +246,8 @@ ENV _APP_SERVER=swoole \
_APP_SETUP=self-hosted \
_APP_VERSION=$VERSION \
_APP_USAGE_STATS=enabled \
_APP_USAGE_TIMESERIES_INTERVAL=30 \
_APP_USAGE_DATABASE_INTERVAL=900 \
# 14 Days = 1209600 s
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600 \
_APP_MAINTENANCE_RETENTION_AUDIT=1209600 \
@ -263,6 +304,8 @@ COPY --from=imagick /usr/local/lib/php/extensions/no-debug-non-zts-20200930/imag
COPY --from=yaml /usr/local/lib/php/extensions/no-debug-non-zts-20200930/yaml.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=maxmind /usr/local/lib/php/extensions/no-debug-non-zts-20200930/maxminddb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=scrypt /usr/local/lib/php/extensions/php-scrypt/target/libphp_scrypt.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=zstd /usr/local/lib/php/extensions/no-debug-non-zts-20200930/zstd.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
# Add Source Code
COPY ./app /usr/src/code/app
@ -319,6 +362,8 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini
RUN echo extension=imagick.so >> /usr/local/etc/php/conf.d/imagick.ini
RUN echo extension=yaml.so >> /usr/local/etc/php/conf.d/yaml.ini
RUN echo extension=maxminddb.so >> /usr/local/etc/php/conf.d/maxminddb.ini
RUN echo extension=libphp_scrypt.so >> /usr/local/etc/php/conf.d/libphp_scrypt.ini
RUN echo extension=zstd.so >> /usr/local/etc/php/conf.d/zstd.ini
RUN if [ "$DEBUG" == "true" ]; then printf "zend_extension=yasd \nyasd.debug_mode=remote \nyasd.init_file=/usr/local/dev/yasd_init.php \nyasd.remote_port=9005 \nyasd.log_level=-1" >> /usr/local/etc/php/conf.d/yasd.ini; fi
RUN if [ "$DEBUG" == "true" ]; then echo "opcache.enable=0" >> /usr/local/etc/php/conf.d/appwrite.ini; fi

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,7 @@ return [
],
Exception::GENERAL_PHONE_DISABLED => [
'name' => Exception::GENERAL_PHONE_DISABLED,
'description' => 'Phone provider is not configured. Please check the _APP_PHONE_PROVIDER environment variable of your Appwrite server.',
'description' => 'Phone provider is not configured. Please check the _APP_SMS_PROVIDER environment variable of your Appwrite server.',
'code' => 503,
],
Exception::GENERAL_ARGUMENT_INVALID => [
@ -102,7 +102,7 @@ return [
],
Exception::USER_ALREADY_EXISTS => [
'name' => Exception::USER_ALREADY_EXISTS,
'description' => 'A user with the same email ID already exists in your project.',
'description' => 'A user with the same email already exists in your project.',
'code' => 409,
],
Exception::USER_BLOCKED => [
@ -122,12 +122,12 @@ return [
],
Exception::USER_EMAIL_NOT_WHITELISTED => [
'name' => Exception::USER_EMAIL_NOT_WHITELISTED,
'description' => 'The user\'s email is not part of the whitelist. Please check the _APP_CONSOLE_WHITELIST_EMAILS environment variable of your Appwrite server.',
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
'code' => 401,
],
Exception::USER_IP_NOT_WHITELISTED => [
'name' => Exception::USER_IP_NOT_WHITELISTED,
'description' => 'The user\'s IP address is not part of the whitelist. Please check the _APP_CONSOLE_WHITELIST_IPS environment variable of your Appwrite server.',
'description' => 'Console registration is restricted to specific IPs. Contact your administrator for more information.',
'code' => 401,
],
Exception::USER_INVALID_CREDENTIALS => [
@ -152,7 +152,7 @@ return [
],
Exception::USER_EMAIL_ALREADY_EXISTS => [
'name' => Exception::USER_EMAIL_ALREADY_EXISTS,
'description' => 'Another user with the same email already exists in the current project.',
'description' => 'A user with the same email already exists in the current project.',
'code' => 409,
],
Exception::USER_PASSWORD_MISMATCH => [
@ -185,6 +185,11 @@ return [
'description' => 'The current user does not have a phone number associated with their account.',
'code' => 400,
],
Exception::USER_MISSING_ID => [
'name' => Exception::USER_MISSING_ID,
'description' => 'Missing ID from OAuth2 provider.',
'code' => 400,
],
/** Teams */
Exception::TEAM_NOT_FOUND => [
@ -194,7 +199,7 @@ return [
],
Exception::TEAM_INVITE_ALREADY_EXISTS => [
'name' => Exception::TEAM_INVITE_ALREADY_EXISTS,
'description' => 'The current user has already received an invitation to join the team.',
'description' => 'User has already been invited or is already a member of this team',
'code' => 409,
],
Exception::TEAM_INVITE_NOT_FOUND => [
@ -218,13 +223,17 @@ return [
'code' => 401,
],
/** Membership */
Exception::MEMBERSHIP_NOT_FOUND => [
'name' => Exception::MEMBERSHIP_NOT_FOUND,
'description' => 'Membership with the requested ID could not be found.',
'code' => 404,
],
Exception::MEMBERSHIP_ALREADY_CONFIRMED => [
'name' => Exception::MEMBERSHIP_ALREADY_CONFIRMED,
'description' => 'Membership already confirmed',
'code' => 409,
],
/** Avatars */
Exception::AVATAR_SET_NOT_FOUND => [
@ -271,7 +280,7 @@ return [
],
Exception::STORAGE_FILE_TYPE_UNSUPPORTED => [
'name' => Exception::STORAGE_FILE_TYPE_UNSUPPORTED,
'description' => 'The file type is not supported.',
'description' => 'The given file extension is not supported.',
'code' => 400,
],
Exception::STORAGE_INVALID_FILE_SIZE => [
@ -325,7 +334,7 @@ return [
],
Exception::BUILD_NOT_READY => [
'name' => Exception::BUILD_NOT_READY,
'description' => 'Build with the requested ID is builing and not ready for execution.',
'description' => 'Build with the requested ID is building and not ready for execution.',
'code' => 400,
],
Exception::BUILD_IN_PROGRESS => [
@ -348,6 +357,19 @@ return [
'code' => 404,
],
/** Databases */
Exception::DATABASE_NOT_FOUND => [
'name' => Exception::DATABASE_NOT_FOUND,
'description' => 'Database not found',
'code' => 404
],
Exception::DATABASE_ALREADY_EXISTS => [
'name' => Exception::DATABASE_ALREADY_EXISTS,
'description' => 'Database already exists',
'code' => 409
],
/** Collections */
Exception::COLLECTION_NOT_FOUND => [
'name' => Exception::COLLECTION_NOT_FOUND,
@ -469,19 +491,24 @@ return [
],
Exception::PROJECT_INVALID_SUCCESS_URL => [
'name' => Exception::PROJECT_INVALID_SUCCESS_URL,
'description' => 'Invalid URL received for OAuth success redirect.',
'description' => 'Invalid redirect URL for OAuth success.',
'code' => 400,
],
Exception::PROJECT_INVALID_FAILURE_URL => [
'name' => Exception::PROJECT_INVALID_FAILURE_URL,
'description' => 'Invalid URL received for OAuth failure redirect.',
'description' => 'Invalid redirect URL for OAuth failure.',
'code' => 400,
],
Exception::PROJECT_MISSING_USER_ID => [
'name' => Exception::PROJECT_MISSING_USER_ID,
'description' => 'Failed to obtain user ID from the OAuth provider.',
Exception::PROJECT_RESERVED_PROJECT => [
'name' => Exception::PROJECT_RESERVED_PROJECT,
'description' => 'The project ID is reserved. Please choose another project ID.',
'code' => 400,
],
Exception::PROJECT_KEY_EXPIRED => [
'name' => Exception::PROJECT_KEY_EXPIRED,
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
'code' => 401,
],
Exception::WEBHOOK_NOT_FOUND => [
'name' => Exception::WEBHOOK_NOT_FOUND,
'description' => 'Webhook with the requested ID could not be found.',
@ -507,9 +534,19 @@ return [
'description' => 'A Domain with the requested ID already exists.',
'code' => 409,
],
Exception::VARIABLE_NOT_FOUND => [
'name' => Exception::VARIABLE_NOT_FOUND,
'description' => 'Variable with the requested ID could not be found.',
'code' => 404,
],
Exception::VARIABLE_ALREADY_EXISTS => [
'name' => Exception::VARIABLE_ALREADY_EXISTS,
'description' => 'Variable with the same ID already exists in your project.',
'code' => 409,
],
Exception::DOMAIN_VERIFICATION_FAILED => [
'name' => Exception::DOMAIN_VERIFICATION_FAILED,
'description' => 'Domain verification for the requested domain has failed.',
'code' => 401,
]
],
];

View file

@ -15,7 +15,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '9.0.1',
'version' => '9.1.0-RC1',
'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' => '6.0.0',
'version' => '8.0.0-dev.1',
'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.6.0',
'version' => '0.7.0-RC1',
'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.7.0',
'version' => '0.8.0-SNAPSHOT',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@ -162,7 +162,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '6.0.0',
'version' => '6.1.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-console',
'package' => '',
'enabled' => true,
@ -180,7 +180,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '0.18.3',
'version' => '0.19.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@ -208,7 +208,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '7.0.2',
'version' => '7.1.0-RC1',
'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' => '5.0.1',
'version' => '5.1.0-RC1',
'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' => '6.0.0',
'version' => '6.1.0-RC1',
'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.10.0',
'version' => '0.11.0-RC1',
'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' => '6.0.0',
'version' => '6.1.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@ -298,7 +298,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => '0.1.0',
'version' => '0.2.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => '',
'enabled' => false,
@ -316,7 +316,7 @@ return [
[
'key' => 'java',
'name' => 'Java',
'version' => '0.0.2',
'version' => '0.0.3-SNAPSHOT',
'url' => 'https://github.com/appwrite/sdk-for-java',
'package' => '',
'enabled' => false,
@ -334,7 +334,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.4.0',
'version' => '0.5.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => false,
@ -352,7 +352,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '6.0.0',
'version' => '7.0.0-dev.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.6.0',
'version' => '0.7.0-SNAPSHOT',
'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.6.0',
'version' => '0.7.0-RC1',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,

View file

@ -52,20 +52,23 @@ $admins = [
];
return [
Auth::USER_ROLE_GUEST => [
'label' => 'Guest',
Auth::USER_ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'public',
'home',
'console',
'documents.read',
'documents.write',
'files.read',
'files.write',
'locale.read',
'avatars.read',
'execution.write',
],
],
Auth::USER_ROLE_MEMBER => [
'label' => 'Member',
Auth::USER_ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member, []),
],
Auth::USER_ROLE_ADMIN => [
@ -80,8 +83,8 @@ return [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins, []),
],
Auth::USER_ROLE_APP => [
'label' => 'Application',
Auth::USER_ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['health.read'],
],
];

View file

@ -7,7 +7,7 @@
use Utopia\App;
use Appwrite\Runtimes\Runtimes;
$runtimes = new Runtimes('v1');
$runtimes = new Runtimes('v2');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));

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

View file

@ -170,13 +170,31 @@ return [
],
[
'name' => '_APP_USAGE_AGGREGATION_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds.',
'description' => 'Deprecated since 1.0.0-RC1, use `_APP_USAGE_TIMESERIES_INTERVAL` and `_APP_USAGE_DATABASE_INTERVAL` instead.',
'introduction' => '0.10.0',
'default' => '30',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_USAGE_TIMESERIES_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to Appwrite Database from Timeseries Database. The default value is 30 seconds.',
'introduction' => '1.0.0-RC1',
'default' => '30',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_USAGE_DATABASE_INTERVAL',
'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes.',
'introduction' => '1.0.0-RC1',
'default' => '900',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_WORKER_PER_CORE',
'description' => 'Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance.',
@ -394,8 +412,8 @@ return [
'description' => '',
'variables' => [
[
'name' => '_APP_PHONE_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'phone://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'name' => '_APP_SMS_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,
@ -403,7 +421,7 @@ return [
'filter' => ''
],
[
'name' => '_APP_PHONE_FROM',
'name' => '_APP_SMS_FROM',
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
'introduction' => '0.15.0',
'default' => '',
@ -804,6 +822,15 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
'introduction' => '1.0.0-RC1',
'default' => '2592000',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MAINTENANCE_RETENTION_EXECUTION',
'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).',

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,6 @@ use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Image\Image;
@ -25,56 +23,34 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
$set = Config::getParam('avatar-' . $type, []);
if (empty($set)) {
throw new Exception('Avatar set not found', 404, Exception::AVATAR_SET_NOT_FOUND);
throw new Exception(Exception::AVATAR_SET_NOT_FOUND);
}
if (!\array_key_exists($code, $set)) {
throw new Exception('Avatar not found', 404, Exception::AVATAR_NOT_FOUND);
throw new Exception(Exception::AVATAR_NOT_FOUND);
}
if (!\extension_loaded('imagick')) {
throw new Exception('Imagick extension is missing', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$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);
$path = $set[$code];
$type = 'png';
if (!\is_readable($path)) {
throw new Exception('File not readable in ' . $path, 500, Exception::GENERAL_SERVER_ERROR);
}
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if ($data) {
//$output = (empty($output)) ? $type : $output;
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
}
$image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data, null);
->file($data)
;
unset($image);
};
@ -82,6 +58,8 @@ App::get('/v1/avatars/credit-cards/:code')
->desc('Get Credit Card Icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/credit-card')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getCreditCard')
@ -100,6 +78,8 @@ App::get('/v1/avatars/browsers/:code')
->desc('Get Browser Icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/browser')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getBrowser')
@ -118,6 +98,8 @@ App::get('/v1/avatars/flags/:code')
->desc('Get Country Flag')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/flag')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFlag')
@ -136,6 +118,8 @@ App::get('/v1/avatars/image')
->desc('Get Image from URL')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/image')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getImage')
@ -151,50 +135,33 @@ App::get('/v1/avatars/image')
$quality = 80;
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/images-' . $url . '-' . $width . '/' . $height . '/' . $quality);
$type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 7/* 1 week */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) {
throw new Exception('Imagick extension is missing', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$fetch = @\file_get_contents($url, false);
if (!$fetch) {
throw new Exception('Image not found', 404, Exception::AVATAR_IMAGE_NOT_FOUND);
throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND);
}
try {
$image = new Image($fetch);
} catch (\Exception $exception) {
throw new Exception('Unable to parse image', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image');
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
unset($image);
});
@ -202,6 +169,8 @@ App::get('/v1/avatars/favicon')
->desc('Get Favicon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/favicon')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFavicon')
@ -217,22 +186,10 @@ App::get('/v1/avatars/favicon')
$height = 56;
$quality = 80;
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/favicon-' . $url);
$type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) {
throw new Exception('Imagick extension is missing', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
$curl = \curl_init();
@ -254,7 +211,7 @@ App::get('/v1/avatars/favicon')
\curl_close($curl);
if (!$html) {
throw new Exception('Failed to fetch remote URL', 404, Exception::AVATAR_REMOTE_URL_FAILED);
throw new Exception(Exception::AVATAR_REMOTE_URL_FAILED);
}
$doc = new DOMDocument();
@ -312,40 +269,31 @@ App::get('/v1/avatars/favicon')
$data = @\file_get_contents($outputHref, false);
if (empty($data) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') {
throw new Exception('Favicon not found', 404, Exception::AVATAR_ICON_NOT_FOUND);
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
}
$cache->save($key, $data);
return $response
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/x-icon')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
}
$fetch = @\file_get_contents($outputHref, false);
if (!$fetch) {
throw new Exception('Icon not found', 404, Exception::AVATAR_ICON_NOT_FOUND);
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND);
}
$image = new Image($fetch);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
unset($image);
});
@ -381,19 +329,21 @@ App::get('/v1/avatars/qr')
}
$image = new Image($qrcode->render($text));
$image->crop((int) $size, (int) $size);
$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')
->desc('Get User Initials')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/initials')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getInitials')
@ -468,5 +418,6 @@ 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());
->file($image->getImageBlob())
;
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,6 @@ App::post('/v1/graphql')
->label('scope', 'public')
->action(
function () {
throw new Exception('GraphQL support is coming soon!', 502, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GraphQL support is coming soon!', 503);
}
);

View file

@ -73,7 +73,7 @@ App::get('/v1/health/db')
$statement->execute();
} catch (Exception $_e) {
throw new Exception('Database is not available', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Database is not available');
}
$output = [
@ -104,7 +104,7 @@ App::get('/v1/health/cache')
$redis = $utopia->getResource('cache');
if (!$redis->ping(true)) {
throw new Exception('Cache is not available', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Cache is not available');
}
$output = [
@ -160,7 +160,7 @@ App::get('/v1/health/time')
$diff = ($timestamp - \time());
if ($diff > $gap || $diff < ($gap * -1)) {
throw new Exception('Server time gaps detected', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Server time gaps detected');
}
$output = [
@ -267,11 +267,11 @@ App::get('/v1/health/storage/local')
$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(Exception::GENERAL_SERVER_ERROR, 'Device ' . $key . ' dir is not readable');
}
if (!\is_writable($device->getRoot())) {
throw new Exception('Device ' . $key . ' dir is not writable', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Device ' . $key . ' dir is not writable');
}
}
@ -315,7 +315,7 @@ App::get('/v1/health/anti-virus')
$output['version'] = @$antivirus->version();
$output['status'] = (@$antivirus->ping()) ? 'pass' : 'fail';
} catch (\Exception $e) {
throw new Exception('Antivirus is not available', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Antivirus is not available');
}
}

View file

@ -17,16 +17,21 @@ use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\DateTime;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Registry\Registry;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -36,7 +41,7 @@ App::init()
->inject('project')
->action(function (Document $project) {
if ($project->getId() !== 'console') {
throw new Exception('Access to this API is forbidden.', 401, Exception::GENERAL_ACCESS_FORBIDDEN);
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
}
});
@ -70,7 +75,7 @@ App::post('/v1/projects')
$team = $dbForConsole->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$auth = Config::getParam('auth', []);
@ -79,16 +84,21 @@ App::post('/v1/projects')
$auths[$method['key'] ?? ''] = true;
}
$projectId = ($projectId == 'unique()') ? $dbForConsole->getId() : $projectId;
$projectId = ($projectId == 'unique()') ? ID::unique() : $projectId;
if ($projectId === 'console') {
throw new Exception("'console' is a reserved project.", 400, Exception::PROJECT_RESERVED_PROJECT);
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
}
$project = $dbForConsole->createDocument('projects', new Document([
'$id' => $projectId,
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner', 'team:' . $teamId . '/developer'],
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'name' => $name,
'teamInternalId' => $team->getInternalId(),
'teamId' => $team->getId(),
@ -101,7 +111,7 @@ App::post('/v1/projects')
'legalState' => $legalState,
'legalCity' => $legalCity,
'legalAddress' => $legalAddress,
'legalTaxId' => $legalTaxId,
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
'authProviders' => [],
@ -127,6 +137,7 @@ App::post('/v1/projects')
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
$attributes = [];
$indexes = [];
@ -153,7 +164,6 @@ App::post('/v1/projects')
'orders' => $index['orders'],
]);
}
$dbForProject->createCollection($key, $attributes, $indexes);
}
@ -171,36 +181,37 @@ App::get('/v1/projects')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT_LIST)
->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the project used as the starting point for the query, excluding the project itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForConsole) {
->action(function (array $queries, string $search, Response $response, Database $dbForConsole) {
if (!empty($cursor)) {
$cursorProject = $dbForConsole->getDocument('projects', $cursor);
if ($cursorProject->isEmpty()) {
throw new Exception("Project '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND);
}
}
$queries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
$queries[] = Query::search('search', $search);
}
$results = $dbForConsole->find('projects', $queries, $limit, $offset, [], [$orderType], $cursorProject ?? null, $cursorDirection);
$total = $dbForConsole->count('projects', $queries, APP_LIMIT_COUNT);
// Get cursor document if there was a cursor query
$cursor = reset(Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE));
if ($cursor) {
/** @var Query $cursor */
$projectId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('projects', $projectId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$response->dynamic(new Document([
'projects' => $results,
'total' => $total,
'projects' => $dbForConsole->find('projects', $queries),
'total' => $dbForConsole->count('projects', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_PROJECT_LIST);
});
@ -222,7 +233,7 @@ App::get('/v1/projects/:projectId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$response->dynamic($project, Response::MODEL_PROJECT);
@ -249,7 +260,7 @@ App::get('/v1/projects/:projectId/usage')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$usage = [];
@ -276,13 +287,13 @@ App::get('/v1/projects/:projectId/usage')
$dbForProject->setNamespace("_{$project->getInternalId()}");
$metrics = [
'requests',
'network',
'executions',
'users.count',
'databases.documents.count',
'databases.collections.count',
'storage.total'
'project.$all.network.requests',
'project.$all.network.bandwidth',
'project.$all.storage.size',
'users.$all.count.total',
'collections.$all.count.total',
'documents.$all.count.total',
'executions.$all.compute.total',
];
$stats = [];
@ -293,9 +304,11 @@ App::get('/v1/projects/:projectId/usage')
$period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [
new Query('period', Query::TYPE_EQUAL, [$period]),
new Query('metric', Query::TYPE_EQUAL, [$metric]),
], $limit, 0, ['time'], [Database::ORDER_DESC]);
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric] = [];
foreach ($requestDocs as $requestDoc) {
@ -315,7 +328,7 @@ App::get('/v1/projects/:projectId/usage')
};
$stats[$metric][] = [
'value' => 0,
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
'date' => DateTime::addSeconds(new \DateTime($stats[$metric][$last]['date'] ?? null), -1 * $diff),
];
$backfill--;
}
@ -325,13 +338,13 @@ App::get('/v1/projects/:projectId/usage')
$usage = new Document([
'range' => $range,
'requests' => $stats['requests'],
'network' => $stats['network'],
'functions' => $stats['executions'],
'documents' => $stats['databases.documents.count'],
'collections' => $stats['databases.collections.count'],
'users' => $stats['users.count'],
'storage' => $stats['storage.total']
'requests' => $stats[$metrics[0]] ?? [],
'network' => $stats[$metrics[1]] ?? [],
'storage' => $stats[$metrics[2]] ?? [],
'users' => $stats[$metrics[3]] ?? [],
'collections' => $stats[$metrics[4]] ?? [],
'documents' => $stats[$metrics[5]] ?? [],
'executions' => $stats[$metrics[6]] ?? [],
]);
}
@ -366,7 +379,7 @@ App::patch('/v1/projects/:projectId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
@ -405,7 +418,7 @@ App::patch('/v1/projects/:projectId/service')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$services = $project->getAttribute('services', []);
@ -437,7 +450,7 @@ App::patch('/v1/projects/:projectId/oauth2')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$providers = $project->getAttribute('authProviders', []);
@ -468,7 +481,7 @@ App::patch('/v1/projects/:projectId/auth/limit')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
@ -503,7 +516,7 @@ App::patch('/v1/projects/:projectId/auth/:method')
$status = ($status === '1' || $status === 'true' || $status === 1 || $status === true);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
@ -531,14 +544,14 @@ App::delete('/v1/projects/:projectId')
->inject('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);
if (!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$deletes
@ -547,11 +560,11 @@ App::delete('/v1/projects/:projectId')
;
if (!$dbForConsole->deleteDocument('teams', $project->getAttribute('teamId', null))) {
throw new Exception('Failed to remove project team from DB', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project team from DB');
}
if (!$dbForConsole->deleteDocument('projects', $projectId)) {
throw new Exception('Failed to remove project from DB', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB');
}
$response->noContent();
@ -583,15 +596,18 @@ App::post('/v1/projects/:projectId/webhooks')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
@ -629,12 +645,13 @@ App::get('/v1/projects/:projectId/webhooks')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$webhooks = $dbForConsole->find('webhooks', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
], 5000);
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'webhooks' => $webhooks,
@ -661,16 +678,16 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
@ -701,18 +718,18 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$webhook
@ -749,16 +766,16 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
@ -787,16 +804,16 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$webhookId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404, Exception::WEBHOOK_NOT_FOUND);
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$dbForConsole->deleteDocument('webhooks', $webhook->getId());
@ -821,26 +838,31 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in DateTime. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
'scopes' => $scopes,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
'secret' => \bin2hex(\random_bytes(128)),
]);
@ -870,12 +892,13 @@ App::get('/v1/projects/:projectId/keys')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$keys = $dbForConsole->find('keys', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()]),
], 5000);
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'keys' => $keys,
@ -902,16 +925,16 @@ App::get('/v1/projects/:projectId/keys/:keyId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404, Exception::KEY_NOT_FOUND);
throw new Exception(Exception::KEY_NOT_FOUND);
}
$response->dynamic($key, Response::MODEL_KEY);
@ -931,24 +954,24 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', null, new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in DateTime. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404, Exception::KEY_NOT_FOUND);
throw new Exception(Exception::KEY_NOT_FOUND);
}
$key
@ -982,16 +1005,16 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$keyId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404, Exception::KEY_NOT_FOUND);
throw new Exception(Exception::KEY_NOT_FOUND);
}
$dbForConsole->deleteDocument('keys', $key->getId());
@ -1025,13 +1048,16 @@ App::post('/v1/projects/:projectId/platforms')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'type' => $type,
@ -1067,12 +1093,13 @@ App::get('/v1/projects/:projectId/platforms')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platforms = $dbForConsole->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
], 5000);
Query::equal('projectId', [$project->getId()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'platforms' => $platforms,
@ -1099,16 +1126,16 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404, Exception::PLATFORM_NOT_FOUND);
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$response->dynamic($platform, Response::MODEL_PLATFORM);
@ -1136,16 +1163,16 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404, Exception::PLATFORM_NOT_FOUND);
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$platform
@ -1180,16 +1207,16 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$platformId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404, Exception::PLATFORM_NOT_FOUND);
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$dbForConsole->deleteDocument('platforms', $platformId);
@ -1220,33 +1247,36 @@ App::post('/v1/projects/:projectId/domains')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$document = $dbForConsole->findOne('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()]),
Query::equal('domain', [$domain]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($document && !$document->isEmpty()) {
throw new Exception('Domain already exists', 409, Exception::DOMAIN_ALREADY_EXISTS);
throw new Exception(Exception::DOMAIN_ALREADY_EXISTS);
}
$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.', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
}
$domain = new Domain($domain);
$domain = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'updated' => \time(),
'updated' => DateTime::now(),
'domain' => $domain->get(),
'tld' => $domain->getSuffix(),
'registerable' => $domain->getRegisterable(),
@ -1280,12 +1310,13 @@ App::get('/v1/projects/:projectId/domains')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domains = $dbForConsole->find('domains', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
], 5000);
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'domains' => $domains,
@ -1312,16 +1343,16 @@ App::get('/v1/projects/:projectId/domains/:domainId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404, Exception::DOMAIN_NOT_FOUND);
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
$response->dynamic($domain, Response::MODEL_DOMAIN);
@ -1346,22 +1377,22 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404, Exception::DOMAIN_NOT_FOUND);
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
$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.', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
}
if ($domain->getAttribute('verification') === true) {
@ -1371,7 +1402,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
$validator = new CNAME($target->get()); // Verify Domain with DNS records
if (!$validator->isValid($domain->getAttribute('domain', ''))) {
throw new Exception('Failed to verify domain', 401, Exception::DOMAIN_VERIFICATION_FAILED);
throw new Exception(Exception::DOMAIN_VERIFICATION_FAILED);
}
@ -1406,16 +1437,16 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404, Exception::PROJECT_NOT_FOUND);
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectInternalId', Query::TYPE_EQUAL, [$project->getInternalId()])
Query::equal('_uid', [$domainId]),
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404, Exception::DOMAIN_NOT_FOUND);
throw new Exception(Exception::DOMAIN_NOT_FOUND);
}
$dbForConsole->deleteDocument('domains', $domain->getId());

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
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;
@ -11,17 +10,27 @@ use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\DateTime;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@ -36,6 +45,7 @@ App::post('/v1/teams')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].create')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'create')
@ -50,17 +60,19 @@ App::post('/v1/teams')
->inject('user')
->inject('dbForProject')
->inject('events')
->inject('audits')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events, Event $audits) {
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? $dbForProject->getId() : $teamId;
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
$team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId ,
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner'],
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId, 'owner')),
Permission::delete(Role::team($teamId, 'owner')),
],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'search' => implode(' ', [$teamId, $name]),
@ -71,18 +83,24 @@ App::post('/v1/teams')
$roles[] = 'owner';
}
$membershipId = $dbForProject->getId();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['user:' . $user->getId(), 'team:' . $team->getId()],
'$write' => ['user:' . $user->getId(), 'team:' . $team->getId() . '/owner'],
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::read(Role::team($team->getId())),
Permission::update(Role::user($user->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($user->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => \time(),
'joined' => \time(),
'invited' => DateTime::now(),
'joined' => DateTime::now(),
'confirm' => true,
'secret' => '',
'search' => implode(' ', [$membershipId, $user->getId()])
@ -98,12 +116,6 @@ App::post('/v1/teams')
$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);
});
@ -119,32 +131,37 @@ App::get('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->param('queries', [], new Teams(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Teams::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of teams to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the team used as the starting point for the query, excluding the team itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
if (!empty($cursor)) {
$cursorTeam = $dbForProject->getDocument('teams', $cursor);
if ($cursorTeam->isEmpty()) {
throw new Exception("Team '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND);
}
}
$queries = [];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
$queries[] = Query::search('search', $search);
}
$results = $dbForProject->find('teams', $queries, $limit, $offset, [], [$orderType], $cursorTeam ?? null, $cursorDirection);
$total = $dbForProject->count('teams', $queries, APP_LIMIT_COUNT);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$teamId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('teams', $teamId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Team '{$teamId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('teams', $queries);
$total = $dbForProject->count('teams', $filterQueries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
'teams' => $results,
@ -171,7 +188,7 @@ App::get('/v1/teams/:teamId')
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$response->dynamic($team, Response::MODEL_TEAM);
@ -182,6 +199,7 @@ App::put('/v1/teams/:teamId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].update')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'update')
@ -194,13 +212,12 @@ App::put('/v1/teams/:teamId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events, EventAudit $audits) {
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$team = $dbForProject->updateDocument('teams', $team->getId(), $team
@ -208,7 +225,6 @@ App::put('/v1/teams/:teamId')
->setAttribute('search', implode(' ', [$teamId, $name])));
$events->setParam('teamId', $team->getId());
$audits->setResource('team/' . $team->getId());
$response->dynamic($team, Response::MODEL_TEAM);
});
@ -218,6 +234,7 @@ App::delete('/v1/teams/:teamId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].delete')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'delete')
@ -229,28 +246,28 @@ App::delete('/v1/teams/:teamId')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('audits')
->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes, EventAudit $audits) {
->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$memberships = $dbForProject->find('memberships', [
new Query('teamId', Query::TYPE_EQUAL, [$teamId]),
], 2000, 0); // TODO fix members limit
Query::equal('teamId', [$teamId]),
Query::limit(2000), // TODO fix members limit
]);
// TODO delete all members individually from the user object
foreach ($memberships as $membership) {
if (!$dbForProject->deleteDocument('memberships', $membership->getId())) {
throw new Exception('Failed to remove membership for team from DB', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership for team from DB');
}
}
if (!$dbForProject->deleteDocument('teams', $teamId)) {
throw new Exception('Failed to remove team from DB', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
}
$deletes
@ -262,12 +279,6 @@ App::delete('/v1/teams/:teamId')
->setPayload($response->output($team, Response::MODEL_TEAM))
;
$audits
->setParam('event', 'teams.delete')
->setParam('resource', 'team/' . $teamId)
->setParam('data', $team->getArrayCopy())
;
$response->noContent();
});
@ -277,6 +288,8 @@ App::post('/v1/teams/:teamId/memberships')
->label('event', 'teams.[teamId].memberships.[membershipId].create')
->label('scope', 'teams.write')
->label('auth.type', 'invites')
->label('audits.resource', 'team/{request.teamId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'createMembership')
@ -295,16 +308,15 @@ App::post('/v1/teams/:teamId/memberships')
->inject('user')
->inject('dbForProject')
->inject('locale')
->inject('audits')
->inject('mails')
->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) {
->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, 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);
throw new Exception(Exception::GENERAL_SMTP_DISABLED);
}
$email = \strtolower($email);
@ -312,10 +324,10 @@ App::post('/v1/teams/:teamId/memberships')
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$invitee = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$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;
@ -324,27 +336,33 @@ App::post('/v1/teams/:teamId/memberships')
$total = $dbForProject->count('users', [], APP_LIMIT_USERS);
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
throw new Exception(Exception::USER_COUNT_EXCEEDED, 'Project registration is restricted. Contact your administrator for more information.');
}
}
try {
$userId = $dbForProject->getId();
$userId = ID::unique();
$invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['user:' . $userId, 'role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* 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(),
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
@ -354,30 +372,35 @@ App::post('/v1/teams/:teamId/memberships')
'search' => implode(' ', [$userId, $email, $name])
])));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS);
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
}
$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);
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
}
$secret = Auth::tokenGenerator();
$membershipId = $dbForProject->getId();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['role:all'],
'$write' => ['user:' . $invitee->getId(), 'team:' . $team->getId() . '/owner'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($invitee->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($invitee->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => \time(),
'joined' => ($isPrivilegedUser || $isAppUser) ? \time() : 0,
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
@ -387,7 +410,7 @@ App::post('/v1/teams/:teamId/memberships')
try {
$membership = Authorization::skip(fn() => $dbForProject->createDocument('memberships', $membership));
} catch (Duplicate $th) {
throw new Exception('User is already a member of this team', 409, Exception::TEAM_INVITE_ALREADY_EXISTS);
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
}
$team->setAttribute('total', $team->getAttribute('total', 0) + 1);
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
@ -397,7 +420,7 @@ App::post('/v1/teams/:teamId/memberships')
try {
$membership = $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(Exception::TEAM_INVITE_ALREADY_EXISTS);
}
}
@ -418,10 +441,6 @@ App::post('/v1/teams/:teamId/memberships')
;
}
$audits
->setResource('team/' . $teamId)
;
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
@ -449,49 +468,52 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of memberships to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the membership used as the starting point for the query, excluding the membership itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor, can be either \'before\' or \'after\'.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForProject')
->action(function (string $teamId, string $search, int $limit, int $offset, string $cursor, string $cursorDirection, string $orderType, Response $response, Database $dbForProject) {
->action(function (string $teamId, array $queries, string $search, Response $response, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
if (!empty($cursor)) {
$cursorMembership = $dbForProject->getDocument('memberships', $cursor);
if ($cursorMembership->isEmpty()) {
throw new Exception("Membership '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND);
}
}
$queries = [new Query('teamId', Query::TYPE_EQUAL, [$teamId])];
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
$queries[] = Query::search('search', $search);
}
// Set internal queries
$queries[] = Query::equal('teamId', [$teamId]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE);
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$membershipId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('memberships', $membershipId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Membership '{$membershipId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$memberships = $dbForProject->find(
collection: 'memberships',
queries: $queries,
limit: $limit,
offset: $offset,
orderTypes: [$orderType],
cursor: $cursorMembership ?? null,
cursorDirection: $cursorDirection
);
$total = $dbForProject->count(
collection:'memberships',
queries: $queries,
collection: 'memberships',
queries: $filterQueries,
max: APP_LIMIT_COUNT
);
@ -535,13 +557,13 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$membership = $dbForProject->getDocument('memberships', $membershipId);
if ($membership->isEmpty() || empty($membership->getAttribute('userId'))) {
throw new Exception('Membership not found', 404, Exception::MEMBERSHIP_NOT_FOUND);
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
@ -560,6 +582,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'updateMembershipRoles')
@ -574,23 +597,22 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$membership = $dbForProject->getDocument('memberships', $membershipId);
if ($membership->isEmpty()) {
throw new Exception('Membership not found', 404, Exception::MEMBERSHIP_NOT_FOUND);
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$profile = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
if ($profile->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
throw new Exception(Exception::USER_NOT_FOUND);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@ -598,7 +620,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$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);
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to modify roles');
}
/**
@ -612,8 +634,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
*/
$dbForProject->deleteCachedDocument('users', $profile->getId());
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId());
@ -632,6 +652,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update.status')
->label('scope', 'public')
->label('audits.resource', 'team/{request.teamId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'updateMembershipStatus')
@ -648,33 +670,32 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('user')
->inject('dbForProject')
->inject('geodb')
->inject('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) {
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, Event $events) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
if ($membership->isEmpty()) {
throw new Exception('Membership not found', 404, Exception::MEMBERSHIP_NOT_FOUND);
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
if ($membership->getAttribute('teamId') !== $teamId) {
throw new Exception('Team IDs don\'t match', 404, Exception::TEAM_MEMBERSHIP_MISMATCH);
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
$team = Authorization::skip(fn() => $dbForProject->getDocument('teams', $teamId));
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
throw new Exception('Secret key not valid', 401, Exception::TEAM_INVALID_SECRET);
throw new Exception(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);
throw new Exception(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($user->isEmpty()) {
@ -682,50 +703,51 @@ 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(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($membership->getAttribute('confirm') === true) {
throw new Exception('Membership already confirmed', 409);
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
}
$membership // Attach user to team
->setAttribute('joined', \time())
->setAttribute('joined', DateTime::now())
->setAttribute('confirm', true)
;
$user
->setAttribute('emailVerification', true)
;
$user->setAttribute('emailVerification', true);
// Log user in
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_LOGIN_LONG);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
Authorization::setRole('user:' . $userId);
Authorization::setRole(Role::user($userId)->toString());
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@ -733,8 +755,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1)));
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
@ -747,8 +767,8 @@ 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, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
@ -765,6 +785,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].delete')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'deleteMembership')
@ -775,38 +796,37 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $events) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
if ($membership->isEmpty()) {
throw new Exception('Invite not found', 404, Exception::TEAM_INVITE_NOT_FOUND);
throw new Exception(Exception::TEAM_INVITE_NOT_FOUND);
}
if ($membership->getAttribute('teamId') !== $teamId) {
throw new Exception('Team IDs don\'t match', 404);
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
}
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
throw new Exception(Exception::USER_NOT_FOUND);
}
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
try {
$dbForProject->deleteDocument('memberships', $membership->getId());
} catch (AuthorizationException $exception) {
throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED);
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (\Exception $exception) {
throw new Exception('Failed to remove membership from DB', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership from DB');
}
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -816,8 +836,6 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
}
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
@ -839,25 +857,24 @@ App::get('/v1/teams/:teamId/logs')
->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)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', 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 */
->action(function (string $teamId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
throw new Exception('Team not found', 404, Exception::TEAM_NOT_FOUND);
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $limit, $offset);

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Database\Role;
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
use Utopia\Logger\Log;
@ -22,6 +23,7 @@ use Appwrite\Utopia\Response\Filters\V13 as ResponseV13;
use Appwrite\Utopia\Response\Filters\V14 as ResponseV14;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@ -30,6 +32,7 @@ use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
@ -45,7 +48,8 @@ App::init()
->inject('user')
->inject('locale')
->inject('clients')
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) {
->inject('servers')
->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers) {
/*
* Request format
*/
@ -89,7 +93,7 @@ App::init()
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
$domainDocument = $dbForConsole->findOne('domains', [Query::orderAsc('_id')]);
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
}
@ -97,7 +101,7 @@ App::init()
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()])
Query::equal('domain', [$domain->get()])
]);
if (!$domainDocument) {
@ -131,11 +135,11 @@ App::init()
}
if ($project->isEmpty()) {
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
throw new AppwriteException('Missing or unknown project ID', 400, AppwriteException::PROJECT_UNKNOWN);
throw new AppwriteException(AppwriteException::PROJECT_UNKNOWN);
}
$referrer = $request->getReferer();
@ -206,7 +210,7 @@ App::init()
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https') {
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException('Method unsupported over HTTP.', 500, AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED);
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP.');
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
@ -239,13 +243,15 @@ App::init()
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))
) {
throw new AppwriteException($originValidator->getDescription(), 403, AppwriteException::GENERAL_UNKNOWN_ORIGIN);
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
}
/*
* ACL Check
*/
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER;
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
@ -289,21 +295,42 @@ App::init()
'name' => $project->getAttribute('name', 'Untitled'),
]);
$role = Auth::USER_ROLE_APP;
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire', 0);
if (!empty($expire) && $expire < \time()) {
throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED);
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
throw new AppwriteException(AppwriteException:: PROJECT_KEY_EXPIRED);
}
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
if ($sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!in_array($sdk, $sdks)) {
array_push($sdks, $sdk);
$key->setAttribute('sdks', $sdks);
/** Update access time as well */
$key->setAttribute('accessedAt', Datetime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
}
}
}
}
Authorization::setRole('role:' . $role);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
@ -316,24 +343,24 @@ App::init()
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
) {
throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED);
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
}
}
if (!\in_array($scope, $scopes)) {
if ($project->isEmpty()) { // Check if permission is denied because project is missing
throw new AppwriteException('Project not found', 404, AppwriteException::PROJECT_NOT_FOUND);
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
throw new AppwriteException($user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')', 401, AppwriteException::GENERAL_UNAUTHORIZED_SCOPE);
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
}
if (false === $user->getAttribute('status')) { // Account is blocked
throw new AppwriteException('Invalid credentials. User is blocked', 401, AppwriteException::USER_BLOCKED);
throw new AppwriteException(AppwriteException::USER_BLOCKED);
}
if ($user->getAttribute('reset')) {
throw new AppwriteException('Password reset is required', 412, AppwriteException::USER_PASSWORD_RESET_REQUIRED);
throw new AppwriteException(AppwriteException::USER_PASSWORD_RESET_REQUIRED);
}
});
@ -445,7 +472,7 @@ App::error()
/** Handle Utopia Errors */
if ($error instanceof Utopia\Exception) {
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
switch ($code) {
case 400:
$error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID);
@ -458,7 +485,7 @@ App::error()
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if (!($error instanceof AppwriteException)) {
$error = new AppwriteException($message, $code, AppwriteException::GENERAL_UNKNOWN, $error);
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
}
switch ($code) { // Don't show 500 errors!
@ -601,32 +628,32 @@ App::get('/.well-known/acme-challenge')
]);
if (!$validator->isValid($token) || \count($uriChunks) !== 4) {
throw new AppwriteException('Invalid challenge token.', 400);
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, 'Invalid challenge token.');
}
$base = \realpath(APP_STORAGE_CERTIFICATES);
$absolute = \realpath($base . '/.well-known/acme-challenge/' . $token);
if (!$base) {
throw new AppwriteException('Storage error', 500, AppwriteException::GENERAL_SERVER_ERROR);
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Storage error');
}
if (!$absolute) {
throw new AppwriteException('Unknown path', 404);
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
}
if (!\substr($absolute, 0, \strlen($base)) === $base) {
throw new AppwriteException('Invalid path', 401);
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
}
if (!\file_exists($absolute)) {
throw new AppwriteException('Unknown path', 404);
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
}
$content = @\file_get_contents($absolute);
if (!$content) {
throw new AppwriteException('Failed to get contents', 500, AppwriteException::GENERAL_SERVER_ERROR);
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Failed to get contents');
}
$response->text($content);

View file

@ -7,6 +7,7 @@ use Utopia\Database\Document;
use Appwrite\Network\Validator\Host;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\App;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
@ -253,36 +254,36 @@ App::post('/v1/mock/tests/general/upload')
$file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size'];
if (is_null($start) || is_null($end) || is_null($size)) {
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header');
}
if ($start > $end || $end > $size) {
throw new Exception('Invalid content-range header', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header');
}
if ($start === 0 && !empty($id)) {
throw new Exception('First chunked request cannot have id header', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'First chunked request cannot have id header');
}
if ($start !== 0 && $id !== 'newfileid') {
throw new Exception('All chunked request must have id header (except first)', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'All chunked request must have id header (except first)');
}
if ($end !== $size && $end - $start + 1 !== $chunkSize) {
throw new Exception('Chunk size must be 5MB (except last chunk)', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB (except last chunk)');
}
if ($end !== $size && $file['size'] !== $chunkSize) {
throw new Exception('Wrong chunk size', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Wrong chunk size');
}
if ($file['size'] > $chunkSize) {
throw new Exception('Chunk size must be 5MB or less', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB or less');
}
if ($end !== $size) {
$response->json([
'$id' => 'newfileid',
'$id' => ID::custom('newfileid'),
'chunksTotal' => $file['size'] / $chunkSize,
'chunksUploaded' => $start / $chunkSize
]);
@ -293,15 +294,15 @@ App::post('/v1/mock/tests/general/upload')
$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);
throw new Exception(Exception::GENERAL_MOCK, 'Wrong file name');
}
if ($file['size'] !== 38756) {
throw new Exception('Wrong file size', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size');
}
if (\md5(\file_get_contents($file['tmp_name'])) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') {
throw new Exception('Wrong file uploaded', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Wrong file uploaded');
}
}
});
@ -374,7 +375,7 @@ App::get('/v1/mock/tests/general/get-cookie')
->action(function (Request $request) {
if ($request->getCookie('cookieName', '') !== 'cookieValue') {
throw new Exception('Missing cookie value', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Missing cookie value');
}
});
@ -395,6 +396,35 @@ App::get('/v1/mock/tests/general/empty')
$response->noContent();
});
/** Endpoint to test if required headers are sent from the SDK */
App::get('/v1/mock/tests/general/headers')
->desc('Get headers')
->groups(['mock'])
->label('scope', 'public')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'general')
->label('sdk.method', 'headers')
->label('sdk.description', 'Return headers from the request')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.model', Response::MODEL_MOCK)
->label('sdk.mock', true)
->inject('request')
->inject('response')
->action(function (Request $request, Response $response) {
$res = [
'x-sdk-name' => $request->getHeader('x-sdk-name'),
'x-sdk-platform' => $request->getHeader('x-sdk-platform'),
'x-sdk-language' => $request->getHeader('x-sdk-language'),
'x-sdk-version' => $request->getHeader('x-sdk-version'),
];
$res = array_map(function ($key, $value) {
return $key . ': ' . $value;
}, array_keys($res), $res);
$res = implode("; ", $res);
$response->dynamic(new Document(['result' => $res]), Response::MODEL_MOCK);
});
App::get('/v1/mock/tests/general/400-error')
->desc('400 Error')
->groups(['mock'])
@ -408,7 +438,7 @@ App::get('/v1/mock/tests/general/400-error')
->label('sdk.response.model', Response::MODEL_ERROR)
->label('sdk.mock', true)
->action(function () {
throw new Exception('Mock 400 error', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Mock 400 error');
});
App::get('/v1/mock/tests/general/500-error')
@ -424,7 +454,7 @@ App::get('/v1/mock/tests/general/500-error')
->label('sdk.response.model', Response::MODEL_ERROR)
->label('sdk.mock', true)
->action(function () {
throw new Exception('Mock 500 error', 500, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Mock 500 error', 500);
});
App::get('/v1/mock/tests/general/502-error')
@ -480,11 +510,11 @@ App::get('/v1/mock/tests/general/oauth2/token')
->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);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid client ID');
}
if ($client_secret != '123456') {
throw new Exception('Invalid client secret', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid client secret');
}
$responseJson = [
@ -495,18 +525,18 @@ App::get('/v1/mock/tests/general/oauth2/token')
if ($grantType === 'authorization_code') {
if ($code !== 'abcdef') {
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid token');
}
$response->json($responseJson);
} elseif ($grantType === 'refresh_token') {
if ($refreshToken !== 'tuvwxyz') {
throw new Exception('Invalid refresh token', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid refresh token');
}
$response->json($responseJson);
} else {
throw new Exception('Invalid grant type', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid grant type');
}
});
@ -520,7 +550,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
->action(function (string $token, Response $response) {
if ($token != '123456') {
throw new Exception('Invalid token', 400, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Invalid token');
}
$response->json([
@ -571,7 +601,7 @@ App::shutdown()
$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);
throw new Exception(Exception::GENERAL_MOCK, 'Failed to read results', 500);
}
$result[$route->getMethod() . ':' . $route->getPath()] = true;
@ -579,7 +609,7 @@ App::shutdown()
$tests = \array_merge($tests, $result);
if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) {
throw new Exception('Failed to save results', 500, Exception::GENERAL_MOCK);
throw new Exception(Exception::GENERAL_MOCK, 'Failed to save results', 500);
}
$response->dynamic(new Document(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']), Response::MODEL_MOCK);

View file

@ -7,17 +7,45 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Stats\Stats;
use Appwrite\Usage\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\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
if (count($parts) !== 2) {
throw new Exception('Too less or too many parts', 400, Exception::GENERAL_ARGUMENT_INVALID);
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$params = match ($namespace) {
'user' => (array)$user,
'request' => $requestParams,
default => $responsePayload,
};
if (array_key_exists($replace, $params)) {
$label = \str_replace($find, $params[$replace], $label);
}
}
return $label;
};
App::init()
->groups(['api'])
@ -39,7 +67,7 @@ App::init()
$route = $utopia->match($request);
if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope
throw new Exception('Missing or unknown project ID', 400, Exception::PROJECT_UNKNOWN);
throw new Exception(Exception::PROJECT_UNKNOWN);
}
/*
@ -53,10 +81,11 @@ App::init()
foreach ($abuseKeyLabel as $abuseKey) {
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
$timeLimit
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath());
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod());
$timeLimitArray[] = $timeLimit;
}
@ -74,38 +103,42 @@ App::init()
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = (new \DateTime($timeLimit->time()))->getTimestamp() + $route->getLabel('abuse-time', 3600);
if ($timeLimit->limit() && ($timeLimit->remaining() < $closestLimit || is_null($closestLimit))) {
$closestLimit = $timeLimit->remaining();
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $timeLimit->limit())
->addHeader('X-RateLimit-Remaining', $timeLimit->remaining())
->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600))
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time)
;
}
$enabled = App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
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);
$enabled // Abuse is enabled
&& !$isAppUser // User is not API key
&& !$isPrivilegedUser // User is not an admin
&& $abuse->check() // Route is rate-limited
) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
/*
* Background Jobs
*/
/*
* Background Jobs
*/
$events
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
->setUser($user);
$mails
->setProject($project)
->setUser($user)
;
->setUser($user);
$audits
->setMode($mode)
@ -113,22 +146,42 @@ App::init()
->setIP($request->getIP())
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
->setUser($user);
$usage
->setParam('projectId', $project->getId())
->setParam('httpRequest', 1)
->setParam('httpUrl', $request->getHostname() . $request->getURI())
->setParam('project.{scope}.network.requests', 1)
->setParam('httpMethod', $request->getMethod())
->setParam('httpPath', $route->getPath())
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->setParam('storage', 0)
;
->setParam('project.{scope}.network.inbound', 0)
->setParam('project.{scope}.network.outbound', 0);
$deletes->setProject($project);
$database->setProject($project);
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$key = md5($request->getURI() . implode('*', $request->getParams()));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 30;
$data = $cache->load($key, $timestamp);
if (!empty($data)) {
$data = json_decode($data, true);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($data['content-type'])
->send(base64_decode($data['payload']))
;
$route->setIsActive(false);
} else {
$response->addHeader('X-Appwrite-Cache', 'miss');
}
}
});
App::init()
@ -151,36 +204,36 @@ App::init()
switch ($route->getLabel('auth.type', '')) {
case 'emailPassword':
if (($auths['emailPassword'] ?? true) === false) {
throw new Exception('Email / Password authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
}
break;
case 'magic-url':
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
throw new Exception('Magic URL authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
}
break;
case 'anonymous':
if (($auths['anonymous'] ?? true) === false) {
throw new Exception('Anonymous authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Anonymous authentication is disabled for this project');
}
break;
case 'invites':
if (($auths['invites'] ?? true) === false) {
throw new Exception('Invites authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
}
break;
case 'jwt':
if (($auths['JWT'] ?? true) === false) {
throw new Exception('JWT authentication is disabled for this project', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'JWT authentication is disabled for this project');
}
break;
default:
throw new Exception('Unsupported authentication route', 501, Exception::USER_AUTH_METHOD_UNSUPPORTED);
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
break;
}
});
@ -198,11 +251,13 @@ App::shutdown()
->inject('database')
->inject('mode')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
if (!empty($events->getEvent())) {
if (empty($events->getPayload())) {
$events->setPayload($response->getPayload());
$events->setPayload($responsePayload);
}
/**
* Trigger functions.
@ -255,7 +310,38 @@ App::shutdown()
}
}
if (!empty($audits->getResource())) {
$route = $utopia->match($request);
$requestParams = $route->getParamsValues();
$user = $audits->getUser();
/**
* Audit labels
*/
$pattern = $route->getLabel('audits.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (!empty($resource) && $resource !== $pattern) {
$audits->setResource($resource);
}
}
$pattern = $route->getLabel('audits.userId', null);
if (!empty($pattern)) {
$userId = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$user = $dbForProject->getDocument('users', $userId);
$audits->setUser($user);
}
if (!empty($audits->getResource()) && !empty($audits->getUser()->getId())) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (!empty($pattern)) {
$audits->setPayload($responsePayload);
}
foreach ($events->getParams() as $key => $value) {
$audits->setParam($key, $value);
}
@ -270,16 +356,81 @@ App::shutdown()
$database->trigger();
}
$route = $utopia->match($request);
/**
* Cache label
*/
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = null;
$data = $response->getPayload();
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$key = md5($request->getURI() . implode('*', $request->getParams()));
$data = json_encode([
'content-type' => $response->getContentType(),
'payload' => base64_encode($data['payload']),
]) ;
$signature = md5($data);
$cacheLog = $dbForProject->getDocument('cache', $key);
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'accessedAt' => $now,
'signature' => $signature,
])));
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
}
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$cache->save($key, $data);
}
}
}
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
$metric = $route->getLabel('usage.metric', '');
$usageParams = $route->getLabel('usage.params', []);
if (!empty($metric)) {
$usage->setParam($metric, 1);
foreach ($usageParams as $param) {
$param = $parseLabel($param, $responsePayload, $requestParams, $user);
$parts = explode(':', $param);
if (count($parts) != 2) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Usage params not properly set');
}
$usage->setParam($parts[0], $parts[1]);
}
}
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$usage
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize())
->setParam('project.{scope}.network.inbound', $request->getSize() + $fileSize)
->setParam('project.{scope}.network.outbound', $response->getSize())
->submit();
}
});

View file

@ -5,6 +5,7 @@ use Appwrite\Utopia\Response;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Domains\Domain;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
@ -289,9 +290,23 @@ App::get('/console/databases/collection')
])
;
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('method', 'databases.getCollection')
->setParam('events', 'load,databases.updateCollection')
->setParam('form', 'collectionPermissions')
->setParam('data', 'project-collection')
->setParam('params', [
'collection-id' => '{{router.params.id}}',
'database-id' => '{{router.params.databaseId}}'
]);
$page = new View(__DIR__ . '/../../views/console/databases/collection.phtml');
$page->setParam('logs', $logs);
$page
->setParam('permissions', $permissions)
->setParam('logs', $logs)
;
$layout
->setParam('title', APP_NAME . ' - Database Collection')
@ -315,7 +330,6 @@ App::get('/console/databases/document')
->action(function (string $databaseId, string $collection, View $layout) {
$logs = new View(__DIR__ . '/../../views/console/comps/logs.phtml');
$logs
->setParam('interval', App::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 0))
->setParam('method', 'databases.listDocumentLogs')
@ -326,12 +340,30 @@ App::get('/console/databases/document')
])
;
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('method', 'databases.getDocument')
->setParam('events', 'load,databases.updateDocument')
->setParam('form', 'documentPermissions')
->setParam('data', 'project-document')
->setParam('permissions', [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
])
->setParam('params', [
'collection-id' => '{{router.params.collection}}',
'database-id' => '{{router.params.databaseId}}',
'document-id' => '{{router.params.id}}',
]);
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', false)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('permissions', $permissions)
->setParam('logs', $logs)
;
@ -349,12 +381,29 @@ App::get('/console/databases/document/new')
->inject('layout')
->action(function (string $databaseId, string $collection, View $layout) {
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('data', 'project-document')
->setParam('form', 'documentPermissions')
->setParam('permissions', [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
])
->setParam('params', [
'collection-id' => '{{router.params.collection}}',
'database-id' => '{{router.params.databaseId}}',
'document-id' => '{{router.params.id}}',
]);
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', true)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('permissions', $permissions)
->setParam('logs', new View())
;
@ -392,11 +441,47 @@ App::get('/console/storage/bucket')
->inject('layout')
->action(function (string $id, Response $response, View $layout) {
$bucketPermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$bucketPermissions
->setParam('method', 'databases.getBucket')
->setParam('events', 'load,databases.updateBucket')
->setParam('data', 'project-bucket')
->setParam('form', 'bucketPermissions')
->setParam('params', [
'bucket-id' => '{{router.params.id}}',
]);
$fileCreatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$fileCreatePermissions
->setParam('form', 'fileCreatePermissions')
->setParam('permissions', [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
$fileUpdatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$fileUpdatePermissions
->setParam('method', 'storage.getFile')
->setParam('data', 'file')
->setParam('form', 'fileUpdatePermissions')
->setParam('permissions', [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
])
->setParam('params', [
'bucket-id' => '{{router.params.id}}',
]);
$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)))
->setParam('bucketPermissions', $bucketPermissions)
->setParam('fileCreatePermissions', $fileCreatePermissions)
->setParam('fileUpdatePermissions', $fileUpdatePermissions)
;
$layout
@ -512,9 +597,9 @@ App::get('/console/version')
if ($version && isset($version['version'])) {
return $response->json(['version' => $version['version']]);
} else {
throw new Exception('Failed to check for a newer version', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to check for a newer version');
}
} catch (\Throwable $th) {
throw new Exception('Failed to check for a newer version', 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to check for a newer version');
}
});

View file

@ -12,6 +12,7 @@ use Swoole\Runtime;
use Swoole\Timer;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Orchestration\Adapter\DockerCLI;
@ -188,8 +189,9 @@ App::post('/v1/runtimes')
$containerId = '';
$stdout = '';
$stderr = '';
$startTime = \time();
$endTime = 0;
$startTime = DateTime::now();
$startTimeUnix = (new \DateTime($startTime))->getTimestamp();
$endTimeUnix = 0;
$orchestration = $orchestrationPool->get();
$secret = \bin2hex(\random_bytes(16));
@ -198,8 +200,8 @@ App::post('/v1/runtimes')
$activeRuntimes->set($runtimeId, [
'id' => $containerId,
'name' => $runtimeId,
'created' => $startTime,
'updated' => $endTime,
'created' => $startTimeUnix,
'updated' => $endTimeUnix,
'status' => 'pending',
'key' => $secret,
]);
@ -262,7 +264,7 @@ App::post('/v1/runtimes')
labels: [
'openruntimes-id' => $runtimeId,
'openruntimes-type' => 'runtime',
'openruntimes-created' => strval($startTime),
'openruntimes-created' => strval($startTimeUnix),
'openruntimes-runtime' => $runtime,
],
workdir: $workdir,
@ -319,28 +321,32 @@ App::post('/v1/runtimes')
$stdout = 'Build Successful!';
}
$endTime = \time();
$endTime = DateTime::now();
$endTimeUnix = (new \DateTime($endTime))->getTimestamp();
$duration = $endTimeUnix - $startTimeUnix;
$container = array_merge($container, [
'status' => 'ready',
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
'startTime' => $startTime,
'endTime' => $endTime,
'duration' => $endTime - $startTime,
'duration' => $duration,
]);
if (!$remove) {
$activeRuntimes->set($runtimeId, [
'id' => $containerId,
'name' => $runtimeId,
'created' => $startTime,
'updated' => $endTime,
'status' => 'Up ' . \round($endTime - $startTime, 2) . 's',
'created' => $startTimeUnix,
'updated' => $endTimeUnix,
'status' => 'Up ' . \round($duration, 2) . 's',
'key' => $secret,
]);
}
Console::success('Build Stage completed in ' . ($endTime - $startTime) . ' seconds');
Console::success('Build Stage completed in ' . ($duration) . ' seconds');
} catch (Throwable $th) {
Console::error('Build failed: ' . $th->getMessage() . $stdout);
@ -488,6 +494,7 @@ App::post('/v1/execution')
$executionStart = \microtime(true);
$stdout = '';
$stderr = '';
$res = '';
$statusCode = 0;
$errNo = -1;
$executorResponse = '';
@ -515,6 +522,7 @@ App::post('/v1/execution')
]);
$executorResponse = \curl_exec($ch);
$executorResponse = json_decode($executorResponse, true);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
@ -538,13 +546,19 @@ App::post('/v1/execution')
switch (true) {
case $statusCode >= 500:
$stderr = $executorResponse ?? 'Internal Runtime error.';
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.';
$stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.';
break;
case $statusCode >= 100:
$stdout = $executorResponse;
$stdout = $executorResponse['stdout'];
$res = $executorResponse['response'];
if (is_array($res)) {
$res = json_encode($res, JSON_UNESCAPED_UNICODE);
}
break;
default:
$stderr = $executorResponse ?? 'Execution failed.';
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.';
$stdout = ($executorResponse ?? [])['stdout'] ?? '';
break;
}
@ -557,7 +571,8 @@ App::post('/v1/execution')
$execution = [
'status' => $functionStatus,
'statusCode' => $statusCode,
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB
'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
'time' => $executionTime,
];
@ -648,7 +663,7 @@ $http->on('start', function ($http) {
/**
* Warmup: make sure images are ready to run fast 🚀
*/
$runtimes = new Runtimes('v1');
$runtimes = new Runtimes('v2');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
$runtimes = $runtimes->getAll(true, $allowList);
foreach ($runtimes as $runtime) {

View file

@ -10,6 +10,9 @@ use Swoole\Http\Response as SwooleResponse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Audit\Audit;
use Utopia\Abuse\Adapters\TimeLimit;
@ -132,7 +135,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
@ -146,7 +149,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
@ -160,17 +163,22 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty()) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
'$id' => 'default',
'$collection' => 'buckets',
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'permission' => 'file',
'maximumFileSize' => (int) App::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
'allowedFileExtensions' => [],
'enabled' => true,
'compression' => 'gzip',
'encryption' => true,
'antivirus' => true,
'$read' => ['role:all'],
'$write' => ['role:all'],
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
@ -187,7 +195,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
@ -201,7 +209,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
@ -252,7 +260,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
try {
Authorization::cleanRoles();
Authorization::setRole('role:all');
Authorization::setRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {

View file

@ -23,12 +23,12 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
@ -40,9 +40,10 @@ use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Database\ID;
use Utopia\Logger\Logger;
use Utopia\Config\Config;
use Utopia\Locale\Locale;
@ -63,6 +64,7 @@ use Swoole\Database\PDOPool;
use Swoole\Database\RedisConfig;
use Swoole\Database\RedisPool;
use Utopia\Database\Query;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Storage\Device;
use Utopia\Storage\Storage;
use Utopia\Storage\Device\Backblaze;
@ -88,11 +90,16 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
const APP_CACHE_BUSTER = 402;
const APP_VERSION_STABLE = '0.15.3';
const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 403;
const APP_VERSION_STABLE = '1.0.0-RC1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
@ -143,6 +150,12 @@ const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
// Compression type
const COMPRESSION_TYPE_NONE = 'none';
const COMPRESSION_TYPE_GZIP = 'gzip';
const COMPRESSION_TYPE_ZSTD = 'zstd';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@ -267,9 +280,10 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('attributes', [
new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]),
new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')])
], $database->getAttributeLimit(), 0, []);
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getAttributeLimit()),
]);
}
);
@ -281,9 +295,10 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('indexes', [
new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]),
new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')])
], 64);
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit(64),
]);
}
);
@ -295,8 +310,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('platforms', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
@ -308,8 +324,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('domains', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
@ -321,8 +338,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
@ -334,8 +352,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('webhooks', [
new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY);
Query::equal('projectInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
);
@ -346,8 +365,9 @@ Database::addFilter(
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn () => $database->find('sessions', [
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
@ -359,8 +379,9 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('tokens', [
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
@ -372,8 +393,22 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('memberships', [
new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()])
], APP_LIMIT_SUBQUERY));
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryVariables',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('variables', [
Query::equal('functionInternalId', [$document->getInternalId()]),
], APP_LIMIT_SUBQUERY);
}
);
@ -410,6 +445,10 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function () {
return new Email();
}, Database::VAR_STRING);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () {
return new DatetimeValidator();
}, Database::VAR_DATETIME);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) {
$elements = $attribute['formatOptions']['elements'];
return new WhiteList($elements, true);
@ -448,7 +487,7 @@ $register->set('logger', function () {
}
if (!Logger::hasProvider($providerName)) {
throw new Exception("Logging provider not supported. Logging disabled.", 500, Exception::GENERAL_SERVER_ERROR);
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Logging provider not supported. Logging is disabled");
}
$classname = '\\Utopia\\Logger\\Adapter\\' . \ucfirst($providerName);
@ -711,7 +750,7 @@ App::setResource('usage', function ($register) {
App::setResource('clients', function ($request, $console, $project) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => 'platforms',
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
'type' => 'web',
'hostname' => $request->getHostname(),
@ -787,7 +826,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
if (APP_MODE_ADMIN !== $mode) {
if ($project->isEmpty()) {
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
@ -799,14 +838,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
) { // Validate user has valid login token
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
if (APP_MODE_ADMIN === $mode) {
if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) {
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
} else {
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
}
@ -818,7 +857,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
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(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
@ -829,7 +868,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
}
@ -854,10 +893,10 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('console', function () {
return new Document([
'$id' => 'console',
'$internalId' => 'console',
'$id' => ID::custom('console'),
'$internalId' => ID::custom('console'),
'name' => 'Appwrite',
'$collection' => 'projects',
'$collection' => ID::custom('projects'),
'description' => 'Appwrite core engine',
'logo' => '',
'teamId' => -1,
@ -865,7 +904,7 @@ App::setResource('console', function () {
'keys' => [],
'platforms' => [
[
'$collection' => 'platforms',
'$collection' => ID::custom('platforms'),
'name' => 'Localhost',
'type' => 'web',
'hostname' => 'localhost',
@ -982,8 +1021,8 @@ App::setResource('geodb', function ($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('phone', function () {
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
@ -997,3 +1036,14 @@ App::setResource('phone', function () {
default => null
};
});
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');
$server = $platforms[APP_PLATFORM_SERVER];
$languages = array_map(function ($language) {
return strtolower($language['name']);
}, $server['languages']);
return $languages;
});

View file

@ -1,7 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response;
@ -14,8 +13,11 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\ID;
use Utopia\Database\Role;
use Utopia\Logger\Log;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Cache;
use Utopia\Database\Adapter\MariaDB;
@ -134,7 +136,7 @@ function getDatabase(Registry &$register, string $namespace)
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started succefully');
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
@ -146,12 +148,11 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
try {
$attempts++;
$document = new Document([
'$id' => $database->getId(),
'$collection' => 'realtime',
'$read' => [],
'$write' => [],
'$id' => ID::unique(),
'$collection' => ID::custom('realtime'),
'$permissions' => [],
'container' => $containerId,
'timestamp' => time(),
'timestamp' => DateTime::now(),
'value' => '{}'
]);
@ -181,7 +182,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
[$database, $returnDatabase] = getDatabase($register, '_console');
$statsDocument
->setAttribute('timestamp', time())
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
@ -203,13 +204,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')) {
if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) {
[$database, $returnDatabase] = getDatabase($register, '_console');
$payload = [];
$list = Authorization::skip(fn () => $database->find('realtime', [
new Query('timestamp', Query::TYPE_GREATER, [(time() - 15)])
Query::greaterThan('timestamp', DateTime::addSeconds(new \DateTime(), -15)),
]));
/**
@ -236,7 +237,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
'data' => [
'events' => ['stats.connections'],
'channels' => ['project'],
'timestamp' => time(),
'timestamp' => DateTime::now(),
'payload' => [
$projectId => $payload[$projectId]
]
@ -254,16 +255,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
/**
* Sending test message for SDK E2E tests every 5 seconds.
*/
if ($realtime->hasSubscriber('console', 'role:guest', 'tests')) {
if ($realtime->hasSubscriber('console', Role::guests()->toString(), 'tests')) {
$payload = ['response' => 'WS:/v1/realtime:passed'];
$event = [
'project' => 'console',
'roles' => ['role:guest'],
'roles' => [Role::guests()->toString()],
'data' => [
'events' => ['test.event'],
'channels' => ['tests'],
'timestamp' => time(),
'timestamp' => DateTime::now(),
'payload' => $payload
]
];
@ -433,7 +434,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_USER);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'connected',
@ -548,7 +549,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
$user = $response->output($user, Response::MODEL_USER);
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'response',
'data' => [

View file

@ -300,6 +300,9 @@
<div
data-service="account.getLogs"
data-scope="console"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}})"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-name="securityLogs"
data-event="load">
@ -339,13 +342,12 @@
<form
data-service="account.getLogs"
data-event="submit"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-scope="console"
data-name="securityLogs"
data-success="state"
data-success-param-state-keys="offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{securityLogs.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-offset="{{router.params.offset}}" data-total="{{securityLogs.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{securityLogs.total|pageTotal}}"></span>
@ -353,13 +355,12 @@
<form
data-service="account.getLogs"
data-event="submit"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-scope="console"
data-name="securityLogs"
data-success="state"
data-success-param-state-keys="offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{securityLogs.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-offset="{{router.params.offset}}" data-total="{{securityLogs.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>

View file

@ -123,7 +123,7 @@
data-analytics-category="console/navigation"
data-analytics-label="Users Link">
<i class="icon-users"></i>
Users
Authentication
</a>
</li>
<li>

View file

@ -8,7 +8,9 @@ $params = $this->getParam('params', []);
<?php foreach($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}})"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-event="load"
data-name="logs">
@ -65,13 +67,12 @@ $params = $this->getParam('params', []);
<?php endforeach; ?>
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{logs.total}}" class="margin-end-small round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-offset="{{router.params.offset}}" data-total="{{logs.total}}" class="margin-end-small round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{logs.total|pageTotal}}"></span>
@ -83,13 +84,12 @@ $params = $this->getParam('params', []);
<?php endforeach; ?>
data-event="submit"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="logs"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{logs.total}}" class="margin-start-small round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-offset="{{router.params.offset}}" data-total="{{logs.total}}" class="margin-start-small round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>

View file

@ -0,0 +1,117 @@
<?php
use Utopia\Database\Database;
$method = $this->getParam('method', '');
$params = $this->getParam('params', []);
$events = $this->getParam('events', '');
$permissions = $this->getParam('permissions', Database::PERMISSIONS);
$data = $this->getParam('data', '');
$form = $this->getParam('form');
$escapedPermissions = \array_map(function ($perm) {
// Alpine won't bind to a parameter named delete
if ($perm == 'delete') {
return 'xdelete';
}
return $perm;
}, $permissions);
?>
<div
x-data="permissionsMatrix"
class="permissions-matrix margin-bottom-large"
data-scope="sdk"
<?php if (!empty($method)): ?>
data-method="<?php echo $method; ?>"
<?php endif; ?>
<?php foreach ($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
<?php if (!empty($events)): ?>
data-events="<?php echo $events; ?>"
<?php endif; ?>
<?php if (!empty($data)): ?>
data-name="<?php echo $data; ?>"
<?php endif; ?>
@reset.window="permissions.length = rawPermissions.length = 0">
<input
type="hidden"
name="permissions"
data-cast-from="csv"
data-cast-to="array"
<?php if (!empty(($data))): ?>
data-ls-bind="{{<?php echo $data ?>.$permissions}}"
<?php endif; ?>
:value="rawPermissions"/>
<table data-ls-attrs="x-init=load({{<?php if (!empty($data)) echo $data . '.$permissions' ?>}})">
<thead>
<tr>
<th>Role</th>
<?php foreach ($permissions as $permission): ?>
<th><?php echo \ucfirst($permission); ?></th>
<?php endforeach; ?>
<th></th>
</tr>
</thead>
<tbody>
<template x-for="(permission, index) in permissions">
<tr>
<td>
<p x-text="permission.role"></p>
</td>
<?php foreach ($escapedPermissions as $permission): ?>
<td>
<input
type="checkbox"
name="<?php echo $permission ?>"
x-model="permission.<?php echo $permission; ?>"
@click="updatePermission(index)"/>
</td>
<?php endforeach; ?>
<td>
<span class="action" @click="removePermission(index)">
<i class="icon-trash"></i>
</span>
</td>
</tr>
</template>
<tr x-data="permissionsRow"
@addrow<?php echo \strtolower($form); ?>.window="addPermission('<?php echo $form; ?>', role, { <?php echo \implode(',', $escapedPermissions) ?> })">
<td>
<datalist id="types">
<option value="user:">
<option value="team:">
<option value="users">
<option value="guests">
<option value="any">
</datalist>
<input
required
id="<?php echo $form; ?>Input"
name="<?php echo $form; ?>"
form="<?php echo $form ?>"
list="types"
type="text"
x-model="role" />
</td>
<?php foreach ($escapedPermissions as $permission): ?>
<td>
<input type="checkbox" name="<?php echo $permission ?>" x-model="<?php echo $permission; ?>"/>
</td>
<?php endforeach; ?>
<td></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="<?php \count($permissions) + 2 ?>">
<button type="button" class="margin-top-small reverse" @click="$dispatch('addrow<?php echo \strtolower($form); ?>')">Add</button>
</td>
</tr>
</tfoot>
</table>
</div>

View file

@ -1,6 +1,7 @@
<?php
$logs = $this->getParam('logs', null);
$permissions = $this->getParam('permissions', null);
?>
<div
@ -51,9 +52,9 @@ $logs = $this->getParam('logs', null);
data-event="load,databases.createDocument,databases.updateDocument,databases.deleteDocument"
data-param-collection-id="{{router.params.id}}"
data-param-database-id="{{router.params.databaseId}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
data-param-order-types-cast-to="array"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-documents"
x-data="{ project: new URLSearchParams(location.search).get('project') }">
@ -111,14 +112,12 @@ $logs = $this->getParam('logs', null);
data-event="submit"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
data-param-order-types-cast-to="array"
data-scope="sdk"
data-name="project-documents"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-documents.total|pageTotal}}"></span>
@ -128,14 +127,12 @@ $logs = $this->getParam('logs', null);
data-event="submit"
data-param-database-id="{{router.params.databaseId}}"
data-param-collection-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-types="DESC"
data-param-order-types-cast-to="array"
data-scope="sdk"
data-name="project-documents"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-documents.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -317,6 +314,9 @@ $logs = $this->getParam('logs', null);
<li>
<div class="link new-attribute-boolean"><i class="avatar icon-boolean"></i> New Boolean Attribute</div>
</li>
<li>
<div class="link new-attribute-datetime"><i class="avatar icon-string"></i> New DateTime Attribute</div>
</li>
<li>
<div class="link new-attribute-url"><i class="avatar icon-link"></i> New URL Attribute</div>
</li>
@ -486,8 +486,8 @@ $logs = $this->getParam('logs', null);
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=documentsCreate,Read=documentsRead,Updated=documentsUpdate,Deleted=documentsDelete"
data-show-y-axis="true"
@ -510,6 +510,8 @@ $logs = $this->getParam('logs', null);
<div class="row responsive margin-top-negative">
<div class="col span-8 margin-bottom">
<form id="<?php echo $permissions->getParam('form') ?>"></form>
<form
data-analytics
data-analytics-activity
@ -528,8 +530,6 @@ $logs = $this->getParam('logs', null);
data-failure-param-alert-text="Failed to update collection"
data-failure-param-alert-classname="error">
<label>&nbsp;</label>
<div class="box">
<label for="collection-name">Name</label>
<input name="name" id="collection-name" type="text" autocomplete="off" data-ls-bind="{{project-collection.name}}" data-forms-text-direction required placeholder="Collection Name" maxlength="128" />
@ -538,36 +538,25 @@ $logs = $this->getParam('logs', null);
<input name="enabled" type="hidden" data-forms-switch data-cast-to="boolean" data-ls-bind="{{project-collection.enabled}}" /> &nbsp; Enabled <span class="tooltip" data-tooltip="Mark whether collection is enabled"><i class="icon-info-circled"></i></span>
</div>
<hr class="margin-top-small" />
<label class="margin-bottom-small">Permissions</label>
<p class="text-fade text-size-small">Choose the permissions model for this collection.</p>
<p class="text-fade text-size-small">Configure the permissions for this collection.</p>
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="collection" type="radio" class="margin-top-tiny" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Collection Level</b>
<p class="text-fade margin-top-tiny">With Collection Level permissions, you assign permissions only once in the collection.</p>
<p class="text-fade margin-top-tiny">In this permission level, permissions assigned to collection takes the precedence and documents permissions are ignored.</p>
<div data-ls-if="{{project-collection.permission}} === 'collection'">
<label for="collection-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="collection-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-collection.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<?php echo $permissions->render(); ?>
<label for="collection-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="collection-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-collection.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
</div>
</div>
</div>
<hr class="margin-top-no" />
<label class="margin-bottom-small">Document Security</label>
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-1"><input name="documentSecurity" value="false" type="checkbox" class="margin-top-no" data-ls-bind="{{project-collection.documentSecurity}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
<b>Enabled</b>
<p class="text-fade margin-top-tiny">With Document Security enabled, users will be able to access documents for which they have been granted <b>either</b> Document or Collection permissions.</p>
</div>
</div>
@ -583,10 +572,15 @@ $logs = $this->getParam('logs', null);
<input id="id" type="text" autocomplete="off" placeholder="" data-ls-bind="{{project-collection.$id}}" disabled data-forms-copy class="margin-bottom-no" />
</div>
<label>Database ID</label>
<div class="input-copy margin-bottom">
<input type="text" autocomplete="off" placeholder="" data-ls-bind="{{router.params.databaseId}}" disabled data-forms-copy class="margin-bottom-no" />
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
data-analytics
data-analytics-event="click"
data-analytics-category="console"
@ -594,8 +588,8 @@ $logs = $this->getParam('logs', null);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-collection.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-collection.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-collection.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-collection.$createdAt|date}}"></span></li>
</ul>
<form
@ -683,6 +677,60 @@ $logs = $this->getParam('logs', null);
</form>
</div>
<div data-ui-modal class="modal box close sticky-footer" data-button-alias=".new-attribute-datetime">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Add DateTime Attribute</h1>
<form
id="add-datetime-attribute"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Collection Attribute (datetime)"
data-service="databases.createDatetimeAttribute"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created new attribute successfully"
data-success-param-trigger-events="databases.createAttribute"
data-failure="alert"
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, size: null }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
<input type="hidden" name="collectionId" data-ls-bind="{{router.params.id}}" />
<input type="hidden" name="databaseId" data-ls-bind="{{router.params.databaseId}}" />
<label for="string-key">Attribute ID</label>
<input id="string-key" type="text" class="full-width" name="key" required autocomplete="off" maxlength="36" pattern="^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$" />
<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>
<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>
</div>
<div class="margin-bottom">
<input x-model="array" name="array" class="button switch" type="checkbox" /> &nbsp; Array <span class="tooltip" data-tooltip="Mark whether this attribute should act as an array"><i class="icon-info-circled"></i></span>
</div>
<label for="xdefault">Default Value</label>
<template x-if="!(array || required)">
<input name="xdefault" type="datetime-local" class="margin-bottom-large">
</template>
<template x-if="(array || required)">
<input name="xdefault" type="datetime-local" class="margin-bottom-large" disabled value="">
</template>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<div data-ui-modal class="modal box close sticky-footer" data-button-alias=".new-attribute-integer">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>

View file

@ -35,9 +35,9 @@
data-event="load,databases.createCollection,databases.updateCollection,databases.deleteCollection"
data-param-database-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="ASC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}})"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-collections">
@ -67,13 +67,12 @@
data-param-database-id="{{router.params.id}}"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-collections.total|pageTotal}}"></span>
@ -83,13 +82,12 @@
data-param-database-id="{{router.params.id}}"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-collections"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-collections.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -131,9 +129,8 @@
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-permissions" name="permissions" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-documentSecurity" name="documentSecurity" required data-cast-to="boolean" value="false" />
<hr />
@ -292,8 +289,8 @@
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-json" class="link text-size-small">View as JSON</button></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-database.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-database.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-database.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-database.$createdAt|date}}"></span></li>
</ul>
<form

View file

@ -2,6 +2,7 @@
$new = $this->getParam('new', false);
$logs = $this->getParam('logs', null);
$permissions = $this->getParam('permissions', null);
?>
<div
@ -52,6 +53,8 @@ $logs = $this->getParam('logs', null);
<div class="row responsive">
<div class="col span-8 margin-bottom">
<form id="<?php echo $permissions->getParam('form') ?>"></form>
<form
data-analytics
data-analytics-activity
@ -140,6 +143,16 @@ $logs = $this->getParam('logs', null);
:name="attr.key"
:checked="doc[attr.key]" />
</template>
<template x-if="attr.type === 'datetime'">
<input
type="datetime-local"
step=".001"
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
x-model="doc[attr.key]"
data-cast-to="string" />
</template>
<template x-if="attr.type === 'string' && !attr.format">
<textarea
data-forms-text-resize
@ -240,6 +253,16 @@ $logs = $this->getParam('logs', null);
:value="attr.key"
:checked="doc[attr.key][index]" />
</template>
<template x-if="attr.type === 'datetime'">
<input
type="datetime-local"
step=".001"
:placeholder="attr.default"
:name="attr.key"
:required="attr.required"
x-model="doc[attr.key][index]"
data-cast-to="string" />
</template>
<template x-if="attr.type === 'string' && !attr.format">
<textarea
data-forms-text-resize
@ -313,19 +336,13 @@ $logs = $this->getParam('logs', null);
</ul>
</fieldset>
<div class="toggle margin-bottom" data-ls-ui-open data-button-aria="Open Permissions">
<div class="toggle margin-bottom" data-ls-if="{{project-collection.documentSecurity}}" data-ls-ui-open data-button-aria="Open Permissions">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<h3 class="margin-bottom-large">Permissions</h3>
<label for="collection-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="collection-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-document.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<label for="collection-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="collection-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-document.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<?php echo $permissions->render() ?>
</div>
<button data-ls-if="({{project-document.$id}})">Update</button>
@ -348,6 +365,11 @@ $logs = $this->getParam('logs', null);
<input type="text" autocomplete="off" placeholder="" data-ls-bind="{{router.params.collection}}" disabled data-forms-copy class="margin-bottom-no" />
</div>
<label>Database ID</label>
<div class="input-copy margin-bottom">
<input type="text" autocomplete="off" placeholder="" data-ls-bind="{{router.params.databaseId}}" disabled data-forms-copy class="margin-bottom-no" />
</div>
<ul class="margin-bottom-large text-fade text-size-small" data-ls-if="({{project-document.$id}})">
<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"
@ -359,8 +381,8 @@ $logs = $this->getParam('logs', null);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-document.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-document.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-document.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-document.$createdAt|date}}"></span></li>
</ul>
<div data-ls-if="({{project-document.$id}})">

View file

@ -16,9 +16,9 @@
data-service="databases.list"
data-event="load,databases.create,databases.update,databases.delete"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="ASC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}})"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-databases">
@ -47,13 +47,12 @@
data-service="databases.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-databases"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-databases.total|pageTotal}}"></span>
@ -62,13 +61,12 @@
data-service="databases.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="ASC"
data-scope="sdk"
data-name="project-databases"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-databases.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>

View file

@ -91,9 +91,9 @@ sort($patterns);
data-event="load,functions.createDeployment,functions.deleteDeployment,functions.updateDeployment,functions.retryBuild"
data-name="project-function-deployments"
data-param-function-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-success="trigger"
data-success-param-trigger-events="functions.listDeployments">
@ -217,13 +217,12 @@ sort($patterns);
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-function-deployments"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-function-deployments.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-function-deployments.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-function-deployments.total|pageTotal}}"></span>
@ -233,13 +232,12 @@ sort($patterns);
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-function-deployments"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-function-deployments.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-function-deployments.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>
@ -260,8 +258,8 @@ sort($patterns);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|date}}"></span></li>
</ul>
<form name="functions.delete" class="margin-bottom"
@ -291,7 +289,7 @@ sort($patterns);
<li data-state="/console/functions/function/monitors?id={{router.params.id}}&project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-function-id="{{router.params.id}}"
@ -302,9 +300,10 @@ sort($patterns);
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-range="30d"
data-param-function-id="{{router.params.id}}">
<button class="tick">30d</button>
</form>
@ -312,7 +311,7 @@ sort($patterns);
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="submit"
data-name="usage"
data-param-function-id="{{router.params.id}}"
@ -325,44 +324,44 @@ sort($patterns);
<h2>Monitors</h2>
<div
data-service="functions.getUsage"
data-service="functions.getFunctionUsage"
data-event="load"
data-name="usage"
data-param-function-id="{{router.params.id}}">
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=functionsExecutions" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=executionsTotal" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Executions <span data-ls-bind="({{usage.functionsExecutions|statsGetLast|statsTotal}})"></span></li>
<li>Executions <span data-ls-bind="({{usage.executionsTotal|statsGetLast|statsTotal}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=functionsCompute" data-colors="orange" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=executionsTime" data-colors="orange" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="orange">CPU Time <span data-ls-bind="({{usage.functionsCompute|statsGetLast|seconds2hum}})"></span></li>
<li class="orange">CPU Time <span data-ls-bind="({{usage.executionsTime|statsGetLast|seconds2hum}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=functionsFailures" data-colors="red" data-height="140" data-show-y-axis="true" />
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=executionsFailure" data-colors="red" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="red">Errors <span data-ls-bind="({{usage.functionsFailures|statsGetLast|statsTotal}})"></span></li>
<li class="red">Errors <span data-ls-bind="({{usage.executionsFailure|statsGetLast|statsTotal}})"></span></li>
</ul>
</div>
</li>
@ -379,9 +378,9 @@ sort($patterns);
data-event="load,functions.createExecution"
data-name="project-function-executions"
data-param-function-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-success="trigger"
data-success-param-trigger-events="functions.listExecutions">
@ -392,10 +391,10 @@ sort($patterns);
<tr>
<th width="30"></th>
<th width="160">Created</th>
<th width="150">Status</th>
<th width="120">Trigger</th>
<th width="80">Runtime</th>
<th></th>
<th width="100">Status</th>
<th width="80">Trigger</th>
<th width="60">Runtime</th>
<th width=""></th>
</tr>
</thead>
<tbody data-ls-loop="project-function-executions.executions" data-ls-as="execution">
@ -416,29 +415,44 @@ sort($patterns);
<td data-title="Trigger: ">
<span data-ls-bind="{{execution.trigger}}"></span>
</td>
<td data-title="Runtime: ">
<td data-title="Time: ">
<span data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-ls-bind="{{execution.time|seconds2hum}}"></span>
<span data-ls-if="{{execution.status}} === 'waiting' || {{execution.status}} === 'processing'">-</span>
</td>
<td data-title="">
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="">
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="" style="display: flex;">
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-response-{{execution.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>STDOUT</h1>
<h1>RESPONSE</h1>
<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.response.length}})">
<p>No Response was logged.</p>
</div>
</div>
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>STDOUT</h1>
<div class="margin-bottom ide" data-ls-if="({{execution.stdout.length}})">
<pre data-ls-bind="{{execution.stdout}}"></pre>
</div>
<div class="margin-bottom" data-ls-if="(!{{execution.stdout.length}})">
<p>No output was logged.</p>
</div>
</div>
@ -470,13 +484,12 @@ sort($patterns);
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-function-executions"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-function-executions.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-function-executions.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-function-executions.total|pageTotal}}"></span>
@ -486,13 +499,12 @@ sort($patterns);
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-function-executions"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-function-executions.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-function-executions.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>
@ -506,6 +518,158 @@ sort($patterns);
</div>
</div>
</li>
<li data-state="/console/functions/function/variables?id={{router.params.id}}&project={{router.params.project}}">
<h2
data-service="functions.listVariables"
data-event="load,project.update,functions.createVariable,functions.updateVariable,functions.deleteVariable"
data-name="function-variables"
data-param-queries="limit(100)"
data-param-queries-cast-to="array" data-param-queries-cast-from="csv"
data-param-function-id="{{router.params.id}}"
data-scope="sdk">Variables</h2>
<div class="box margin-bottom" data-ls-if="0 < {{function-variables.variables.length}} && undefined !== {{function-variables.variables}}">
<ul data-ls-loop="function-variables.variables" data-ls-as="variable" class="list">
<li class="clear">
<div class="pull-end desktops-only">
<button data-ls-ui-trigger="variable-delete-{{variable.$id}}" class="reverse danger margin-end-small">Delete</button>
<button data-ls-ui-trigger="variable-update-{{variable.$id}}">Update</button>
</div>
<div data-ui-modal data-button-hide="on" data-open-event="variable-update-{{variable.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Update Variable</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="sdk"
data-analytics-label="Update Function Variable"
data-service="functions.updateVariable"
data-scope="sdk"
data-event="submit"
data-success="alert,trigger"
data-success-param-alert-text="Updated variable successfully"
data-success-param-trigger-events="functions.updateVariable"
data-failure="alert,trigger"
data-failure-param-trigger-events="functions.updateVariable"
data-failure-param-alert-text="Failed to update variable"
data-failure-param-alert-classname="error">
<input type="hidden" name="functionId" data-ls-bind="{{router.params.id}}" />
<input type="hidden" name="variableId" data-ls-bind="{{variable.$id}}" />
<label for="name">Name <span class="tooltip large" data-tooltip="The name of the environment variable you want to set"><i class="icon-question"></i></span></label>
<input type="text" class="full-width" name="key" required autocomplete="off" placeholder="APP_ENV" maxlength="128" data-ls-attrs="id=key-{{variable.$id}}" data-ls-bind="{{variable.key}}" />
<label for="key">Value <span class="tooltip large" data-tooltip="The value of your environment variable"><i class="icon-question"></i></span></label>
<input type="text" class="full-width" name="value" required autocomplete="off" placeholder="Production" data-ls-attrs="id=value-{{variable.$id}}}" data-ls-bind="{{variable.value}}" />
<hr />
<button type="submit">Update</button> &nbsp; <button data-ls-ui-trigger="modal-close" type="button" class="reverse">Cancel</button>
</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 Function Variable"
data-service="functions.deleteVariable"
data-scope="sdk"
data-event="variable-delete-{{variable.$id}}"
data-confirm="Are you sure you want to delete this variable?"
data-success="alert,trigger"
data-success-param-alert-text="Deleted variable successfully"
data-success-param-trigger-events="functions.deleteVariable"
data-failure="alert"
data-failure-param-alert-text="Failed to delete variable"
data-failure-param-alert-classname="error">
<input type="hidden" name="functionId" data-ls-bind="{{router.params.id}}" />
<input type="hidden" name="variableId" data-ls-bind="{{variable.$id}}" />
</form>
<div>
<span class="text-one-liner" data-ls-bind="{{variable.key}}"></span>
</div>
<div data-ui-modal class="modal box close" data-button-text="Show Value" data-button-class="link pull-start margin-end-small margin-top-tiny">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Variable Value</h1>
<form>
<div class="input-copy">
<textarea disabled data-forms-copy data-ls-bind="{{variable.value}}"></textarea>
</div>
<hr />
<div>
<button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</div>
</form>
</div>
<div class="phones-only-inline tablets-only-inline margin-top-small">
<button class="link" data-ls-ui-trigger="variable-update-{{variable.$id}}">Update</button>
<button class="link danger" data-ls-ui-trigger="variable-delete-{{variable.$id}}">Delete</button>
</div>
</li>
</ul>
</div>
<div data-ls-if="(!{{function-variables.variables.length}})" class="box dashboard margin-bottom">
<div class="margin-bottom-small margin-top-small margin-end margin-start">
<h3 class="margin-bottom-small text-bold">There are currently no variables</h3>
<p class="margin-bottom-no">Add your first variable to store information securely.</p>
</div>
</div>
<div data-ui-modal class="modal box close" data-button-alias=".variable-new">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Create a new variable</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create New Function Variable"
data-service="functions.createVariable"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Registered new variable successfully"
data-success-param-trigger-events="functions.createVariable"
data-failure="alert"
data-failure-param-alert-text="Failed to register variable"
data-failure-param-alert-classname="error">
<input type="hidden" name="functionId" data-ls-bind="{{router.params.id}}" />
<label for="name">Name <span class="tooltip large" data-tooltip="The name of the environment variable you want to set"><i class="icon-question"></i></span></label>
<input type="text" class="full-width" name="key" required autocomplete="off" placeholder="APP_ENV" maxlength="128" />
<label for="key">Value <span class="tooltip large" data-tooltip="The value of your environment variable"><i class="icon-question"></i></span></label>
<input type="text" class="full-width" name="value" required autocomplete="off" placeholder="Production" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
<div class="pull-start link variable-new" data-ls-ui-open="" data-button-aria="Add Variable" data-button-text="Add Variable" data-button-class="button" data-blur="1">
</div>
</li>
<li data-state="/console/functions/function/settings?id={{router.params.id}}&project={{router.params.project}}" x-data="events">
<h2>Settings</h2>
@ -537,7 +701,7 @@ sort($patterns);
<label for="execute">Execute Access <span class="tooltip small" data-tooltip="Choose who can execute this function using the client API."><i class="icon-info-circled"></i></span> <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="execute" name="execute" data-forms-tags data-cast-to="json" data-ls-bind="{{project-function.execute}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'any' for wildcard access</div>
<label for="timeout">Timeout (seconds) <span class="tooltip small" data-tooltip="Limit the execution time of your function."><i class="icon-info-circled"></i></span></label>
<input name="timeout" id="function-timeout" type="number" autocomplete="off" data-ls-bind="{{project-function.timeout}}" min="1" max="<?php echo $this->escape($timeout); ?>" data-cast-to="integer" />
@ -619,8 +783,8 @@ sort($patterns);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-function.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-function.$createdAt|date}}"></span></li>
</ul>
<form name="functions.delete" class="margin-bottom"

View file

@ -1,5 +1,6 @@
<?php
$runtimes = $this->getParam('runtimes', []);
$usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
?>
<div class="cover">
<h1 class="zone xl margin-bottom-large">
@ -21,9 +22,9 @@ $runtimes = $this->getParam('runtimes', []);
data-event="load,functions.create,functions.update,functions.delete"
data-name="project-functions"
data-param-project-id="{{router.params.project}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-success="trigger"
data-success-param-trigger-events="functions.list">
@ -58,13 +59,12 @@ $runtimes = $this->getParam('runtimes', []);
data-service="functions.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-functions"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-functions.total|pageTotal}}"></span>
@ -73,13 +73,12 @@ $runtimes = $this->getParam('runtimes', []);
data-service="functions.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-functions"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-functions.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-functions.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -136,5 +135,82 @@ $runtimes = $this->getParam('runtimes', []);
</div>
</div>
</li>
<?php if ($usageStatsEnabled): ?>
<li data-state="/console/functions/usage?id={{router.params.id}}&project={{router.params.project}}">
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="90d">
<button class="tick">90d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="30d">
<button class="tick">30d</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
data-service="functions.getUsage"
data-event="submit"
data-name="usage"
data-param-range="24h">
<button class="tick">24h</button>
</form>
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
<h2>Usage</h2>
<div
data-service="functions.getUsage"
data-event="load"
data-name="usage">
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=executionsTotal" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li>Executions <span data-ls-bind="({{usage.executionsTotal|statsGetLast|statsTotal}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=executionsTime" data-colors="orange" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="orange">CPU Time <span data-ls-bind="({{usage.executionsTime|statsGetLast|seconds2hum}})"></span></li>
</ul>
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=executionsFailure" data-colors="red" data-height="140" data-show-y-axis="true" />
</div>
</div>
</div>
<ul class="chart-notes margin-bottom-large">
<li class="red">Errors <span data-ls-bind="({{usage.executionsFailure|statsGetLast|statsTotal}})"></span></li>
</ul>
</div>
</li>
<?php endif;?>
</ul>
</div>

View file

@ -20,9 +20,9 @@ $home = $this->getParam('home', '');
<div class="margin-bottom-xl"
data-service="projects.list"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="console"
data-event="load,projects.create"
data-name="console-projects"
@ -56,13 +56,12 @@ $home = $this->getParam('home', '');
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="console"
data-name="console-projects"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{console-projects.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{console-projects.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{console-projects.total|pageTotal}}"></span>
@ -72,13 +71,12 @@ $home = $this->getParam('home', '');
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="console"
data-name="console-projects"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{console-projects.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{console-projects.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>

View file

@ -1,4 +1,8 @@
<?php
use Utopia\Database\Permission;
use Utopia\Database\Role;
$services = $this->getParam('services', []);
$customDomainsEnabled = $this->getParam('customDomainsEnabled', false);
$customDomainsTarget = $this->getParam('customDomainsTarget', false);
@ -57,24 +61,11 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="logo">Project Logo</label>
<div class="text-align-center clear">
<input type="hidden" name="logo" data-ls-bind="{{console-project.logo}}" data-read="<?php echo $this->escape(json_encode(['role:all'])); ?>" data-write="<?php echo $this->escape(json_encode(['team:{{console-project.teamId}}'])); ?>" data-accept="image/*" data-forms-upload="" data-label-button="Upload" data-preview-alt="Project Logo" data-scope="console" data-default="">
<input type="hidden" name="logo" data-ls-bind="{{console-project.logo}}" data-permissions="<?php echo $this->escape(\json_encode([Permission::read(Role::any()), Permission::update(Role::team('{{console-project.teamId}}')), Permission::delete(Role::team('{{console-project.teamId}}'))])); ?>" data-accept="image/*" data-forms-upload="" data-label-button="Upload" data-preview-alt="Project Logo" data-scope="console" data-default="">
</div>
<hr />
<!-- <div data-ls-if="0 !== {{console-domains|activeDomainsCount}}">
<label for="name">Custom API Endpoints</label>
<ul data-ls-loop="console-domains" data-ls-as="domain">
<li>
<div class="input-copy" data-ls-if="true === {{domain.verification}} && {{domain.certificateId}}">
<input data-forms-copy type="text" disabled data-ls-bind="{{env.PROTOCOL}}://{{domain.domain}}/v1" />
</div>
</li>
</ul>
</div> -->
<button class="" type="submit">Update</button>
</form>
</div>
@ -144,57 +135,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</div>
</div>
</li>
<!-- <li data-state="/console/settings/privacy?project={{router.params.project}}">
<form
data-service="projects.update"
data-scope="console"
data-event="submit"
data-param-project-id="{{router.params.project}}"
data-success="alert,trigger"
data-success-param-alert-text="Updated project successfully"
data-success-param-trigger-events="projects.update"
data-failure="alert"
data-failure-param-alert-text="Failed to update project"
data-failure-param-alert-classname="error">
<h2>Privacy & Legal</h2>
<div class="box margin-bottom">
<input name="$id" type="hidden" data-ls-bind="{{console-project.$id}}" />
<div class="row thin">
<div class="col span-6">
<label for="legalName">Legal Name</label>
<input name="legalName" id="legalName" type="text" autocomplete="off" data-ls-bind="{{console-project.legalName}}" data-forms-text-direction>
<label for="legalCountry">Country</label>
<select id="legalCountry" name="legalCountry" data-ls-bind="{{console-project.legalCountry}}" data-ls-loop="locale-countries" data-ls-as="option">
<option data-ls-attrs="value={{$index}}" data-ls-bind="{{option}}"></option>
</select>
<label for="legalCity">City</label>
<input name="legalCity" id="legalCity" type="text" autocomplete="off" data-ls-bind="{{console-project.legalCity}}" data-forms-text-direction>
</div>
<div class="col span-6">
<label for="legalTaxId">Tax ID</label>
<input name="legalTaxId" id="legalTaxId" type="text" autocomplete="off" data-ls-bind="{{console-project.legalTaxId}}" data-forms-text-direction>
<label for="legalState">State</label>
<input name="legalState" id="legalState" type="text" autocomplete="off" data-ls-bind="{{console-project.legalState}}" data-forms-text-direction>
<label for="legalAddress">Address</label>
<input name="legalAddress" id="legalAddress" type="text" autocomplete="off" data-ls-bind="{{console-project.legalAddress}}" data-forms-text-direction>
</div>
</div>
<hr />
<button class="" type="submit">Update</button>
</div>
</form>
</li> -->
<li data-state="/console/settings/services?project={{router.params.project}}">
<h2>Services</h2>

View file

@ -2,6 +2,9 @@
$home = $this->getParam('home', '');
$fileLimit = $this->getParam('fileLimit', 0);
$fileLimitHuman = $this->getParam('fileLimitHuman', 0);
$bucketPermissions = $this->getParam('bucketPermissions', null);
$fileCreatePermissions = $this->getParam('fileCreatePermissions', null);
$fileUpdatePermissions = $this->getParam('fileUpdatePermissions', null);
?>
<div
@ -34,6 +37,11 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
<div class="zone xl">
<!-- Required for permission input validation -->
<form id="<?php echo $bucketPermissions->getParam('form') ?>"></form>
<form id="<?php echo $fileCreatePermissions->getParam('form') ?>"></form>
<form id="<?php echo $fileUpdatePermissions->getParam('form') ?>"></form>
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/storage/bucket?id={{router.params.id}}&project={{router.params.project}}">
<h2 class="margin-bottom">Files</h2>
@ -43,9 +51,9 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-event="submit"
data-param-bucket-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-files"
data-success="state"
@ -65,9 +73,9 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-event="load,storage.createFile,storage.updateFile,storage.deleteFile"
data-param-bucket-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-files">
@ -100,7 +108,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<td class="col-name" data-title="Name: " data-ls-attrs="title={{file.name}}" >
<div class="trim-inner-text">
<span data-ls-ui-trigger="modal-file-update-{{file.$id}}" class="link text-one-liner" data-ls-bind="{{file.name}}"></span>
<div data-ls-attrs="data-open-event=modal-file-update-{{file.$id}}" data-button-hide="1" data-ui-modal class="box modal sticky-footer width-large close" >
<div data-ls-attrs="data-open-event=modal-file-update-{{file.$id}}" data-button-hide="1" data-ui-modal class="box modal width-xlarge sticky-footer close" >
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Update File</h1>
@ -131,13 +139,13 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
<input type="hidden" data-ls-attrs="id=file-bucketId-{{file.$id}}" name="bucketId" data-ls-bind="{{file.bucketId}}">
<label for="file-read">Read Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" data-ls-attrs="id=file-read-{{file.$id}}" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{file.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<hr class="margin-start margin-end" />
<label for="file-write">Write Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" data-ls-attrs="id=file-write-{{file.$id}}" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{file.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<h3 class="margin-bottom">Permissions</h3>
<div class="margin-start margin-end margin-bottom">
<?php echo $fileUpdatePermissions->render(); ?>
</div>
</form>
<form class="strip"
@ -190,7 +198,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<span data-ls-bind="{{file.sizeOriginal|humanFileUnit}}"></span>
</div>
<div class="margin-bottom">
<i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Created at: <span data-ls-bind="{{file.$createdAt|dateText}}"></span>
<i class="icon-angle-circled-right margin-start-negative-tiny margin-end-tiny"></i> Created at: <span data-ls-bind="{{file.$createdAt|date}}"></span>
</div>
</div>
</div>
@ -211,7 +219,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<span class="text-fade text-size-small" data-ls-bind="{{file.sizeOriginal|humanFileUnit}}"></span>
</td>
<td data-title="Created: ">
<span class="text-fade text-size-small" data-ls-bind="{{file.$createdAt|dateText}}"></span>
<span class="text-fade text-size-small" data-ls-bind="{{file.$createdAt|date}}"></span>
</td>
<td data-title="" class="cell-options-more" style="overflow: visible">
<div class="drop-list end" data-ls-ui-open="" data-button-aria="File Options" data-button-class="icon-dot-3 reset-inner-button" data-blur="1">
@ -232,13 +240,12 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-bucket-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-files.total|pageTotal}}"></span>
@ -248,13 +255,12 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-event="submit"
data-param-bucket-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-files"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-files.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -270,8 +276,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-analytics-category="console"
data-analytics-label="Create Storage File"
x-data
@submit.prevent="$store.uploader.uploadFile($event.target)"
>
@submit.prevent="$store.uploader.uploadFile($event.target)">
<input type="hidden" name="bucketId" id="files-bucketId" data-ls-bind="{{router.params.id}}">
<label for="fileId">File ID</label>
@ -285,18 +290,19 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
name="fileId"
id="fileId" />
<label for="file-read">File</label>
<label for="file">File</label>
<input type="file" name="file" id="file-file" size="1" required>
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">(Max file size allowed: <?php echo $fileLimitHuman; ?>)</div>
<label for="file-read">Read Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="file-read" name="read" data-forms-tags data-cast-to="json" value="<?php echo htmlentities(json_encode(['role:all'])); ?>" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<div class="toggle margin-bottom" data-ls-if="{{project-bucket.fileSecurity}}" data-ls-ui-open data-button-aria="Open Permissions">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<label for="file-write">Write Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="file-write" name="write" data-forms-tags data-cast-to="json" value="" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<h3 class="margin-bottom-large">Permissions</h3>
<?php echo $fileCreatePermissions->render() ?>
</div>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
@ -381,6 +387,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<div class="row responsive margin-top-negative">
<div class="col span-8 margin-bottom">
<form
data-analytics
data-analytics-activity
@ -407,6 +414,13 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<label for="bucket-maximum-file-size">Maximum File Size (bytes) <span class="tooltip small" data-tooltip="Limit file size allowed in the bucket."><i class="icon-info-circled"></i></span></label>
<input name="maximumFileSize" id="bucket-maximum-file-size" type="number" autocomplete="off" data-ls-bind="{{project-bucket.maximumFileSize}}" min="1" data-cast-to="integer" />
<label for="compression">Compression</label>
<select id="compression" data-ls-bind="{{project-bucket.compression}}" name="compression">
<option value="none">None</option>
<option value="gzip">Gzip</option>
<option value="zstd">Zstd</option>
</select>
<div class="margin-bottom">
<input name="enabled" type="hidden" data-forms-switch data-cast-to="boolean" data-ls-bind="{{project-bucket.enabled}}" /> &nbsp; Enabled <span class="tooltip" data-tooltip="Mark whether bucket is enabled"><i class="icon-info-circled"></i></span>
</div>
@ -426,35 +440,21 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<label class="margin-bottom-small">Permissions</label>
<p class="text-fade text-size-small">Choose the permissions model for this bucket.</p>
<p class="text-fade text-size-small">Configure the permissions for this bucket.</p>
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="bucket" type="radio" class="margin-top-tiny" data-ls-bind="{{project-bucket.permission}}" /></div>
<div class="col span-11">
<b>Bucket Level</b>
<p class="text-fade margin-top-tiny">With Bucket Level permissions, you assign permissions only once in the bucket.</p>
<p class="text-fade margin-top-tiny">In this permission level permissions assigned to bucket takes the precedence and file permissions are ignored</p>
<div data-ls-if="{{project-bucket.permission}} == 'bucket'">
<?php echo $bucketPermissions->render(); ?>
<label for="bucket-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="bucket-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-bucket.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<hr class="margin-top-no" />
<label for="bucket-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="bucket-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-bucket.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
</div>
</div>
</div>
<label class="margin-bottom-small">File Security</label>
<div class="row">
<div class="col span-1"><input name="permission" value="file" type="radio" class="margin-top-no" data-ls-bind="{{project-bucket.permission}}" /></div>
<div class="col span-1"><input name="fileSecurity" value="false" type="checkbox" class="margin-top-no" data-ls-bind="{{project-bucket.fileSecurity}}" /></div>
<div class="col span-11">
<b>File Level</b>
<p class="text-fade margin-top-tiny">With File Level permissions, you have granular access control over every file. Users will only be able to access files for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level file permissions take precedence and bucket permissions are ignored.</p>
<b>Enabled</b>
<p class="text-fade margin-top-tiny">With File Security enabled, users will be able to access files for which they have been granted <b>either</b> File or Bucket permissions.</p>
</div>
</div>
@ -481,8 +481,8 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-bucket.$updatedAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-bucket.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Last Updated: <span data-ls-bind="{{project-bucket.$updatedAt|date}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{project-bucket.$createdAt|date}}"></span></li>
</ul>
<form name="storage.deleteBucket" class="margin-bottom"

View file

@ -16,9 +16,9 @@
data-service="storage.listBuckets"
data-event="load,storage.createBucket,storage.updateBucket,storage.deleteBucket"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-buckets">
@ -46,13 +46,12 @@
data-service="storage.listBuckets"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-buckets"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-buckets.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-buckets.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-buckets.total|pageTotal}}"></span>
@ -61,13 +60,12 @@
data-service="storage.listBuckets"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-buckets"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-buckets.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-buckets.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -108,9 +106,8 @@
<label for="bucket-name">Name</label>
<input type="text" class="full-width" id="bucket-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="bucket-permission" name="permission" required value="bucket" />
<input type="hidden" id="bucket-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-permissions" name="permissions" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-fileSecurity" name="fileSecurity" required value="false" data-cast-to="boolean" />
<hr />

View file

@ -11,7 +11,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<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>
<br />
<span>Users</span>
<span>Authentication</span>
</h1>
</div>
@ -25,9 +25,9 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="users.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-users"
data-success="state"
@ -46,9 +46,9 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="users.list"
data-event="load,users.create,users.update,users.delete"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-users">
@ -100,7 +100,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<span class="tag red">Blocked</span>
</span>
</td>
<td data-title="Created: "><small data-ls-bind="{{user.registration|dateText}}"></small></td>
<td data-title="Created: "><small data-ls-bind="{{user.registration|date}}"></small></td>
</tr>
</tbody>
</table>
@ -112,13 +112,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="users.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-users"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-users.total|pageTotal}}"></span>
@ -127,13 +126,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="users.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-users"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-users.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -193,9 +191,9 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="teams.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset=""
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-teams"
data-success="state"
@ -214,9 +212,9 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="teams.list"
data-event="load,teams.create,teams.update,teams.delete"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-teams">
@ -248,7 +246,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<a data-ls-attrs="href=/console/users/teams/team?id={{team.$id}}&project={{router.params.project}}" data-ls-bind="{{team.name}}" data-ls-attrs="title={{team.name}}"></a>
</td>
<td data-title="Members: "><span data-ls-bind="{{team.total}} members"></span></td>
<td data-title="Date Created: "><small data-ls-bind="{{team.$createdAt|dateText}}"></small></td>
<td data-title="Date Created: "><small data-ls-bind="{{team.$createdAt|date}}"></small></td>
</tr>
</tbody>
</table>
@ -260,13 +258,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="teams.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-teams"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-teams.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-teams.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-teams.total|pageTotal}}"></span>
@ -275,13 +272,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
data-service="teams.list"
data-event="submit"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-teams"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-teams.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-teams.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
@ -329,8 +325,8 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</li>
<li data-state="/console/users/providers?project={{router.params.project}}">
<p data-ls-if="{{console-project.authLimit}} == 0" class="text-fade text-size-small margin-bottom pull-end">Unlimited Users <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Set Limit</a></p>
<p data-ls-if="{{console-project.authLimit}} != 0" class="text-fade text-size-small margin-bottom pull-end"><span data-ls-bind="{{console-project.authLimit|statsTotal}}"></span> Users allowed <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Change Limit</a></p>
<p data-ls-if="{{console-project.authLimit}} == 0" class="text-fade text-size-small margin-bottom pull-end">Unlimited Users <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Set Limit</span></p>
<p data-ls-if="{{console-project.authLimit}} != 0" class="text-fade text-size-small margin-bottom pull-end"><span data-ls-bind="{{console-project.authLimit|statsTotal}}"></span> Users allowed <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Change Limit</span></p>
<h2>Settings</h2>

View file

@ -71,9 +71,9 @@
data-service="teams.getMemberships"
data-event="load,teams.create,teams.update,teams.delete,teams.deleteMembership,teams.createMembership"
data-param-team-id="{{router.params.id}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-offset="{{router.params.offset}}"
data-param-order-type="DESC"
data-param-queries="limit(<?php echo APP_PAGING_LIMIT; ?>),offset({{router.params.offset|orZero}}),orderDesc('')"
data-param-queries-cast-to="array"
data-param-queries-cast-from="csv"
data-scope="sdk"
data-name="project-members">
@ -172,13 +172,12 @@
data-event="submit"
data-param-team-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-members"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-back data-offset="{{router.params.offset}}" data-total="{{project-members.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-back data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-members.total}}" class="margin-end round small" aria-label="Back"><i class="icon-left-open"></i></button>
</form>
<span data-ls-bind="{{router.params.offset|pageCurrent}} / {{project-members.total|pageTotal}}"></span>
@ -188,13 +187,12 @@
data-event="submit"
data-param-team-id="{{router.params.id}}"
data-param-search="{{router.params.search}}"
data-param-limit="<?php echo APP_PAGING_LIMIT; ?>"
data-param-order-type="DESC"
data-scope="sdk"
data-name="project-members"
data-success="state"
data-success-param-state-keys="search,offset">
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-total="{{project-members.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
<input type="hidden" name="offset">
<button name="queries" data-cast-to="array" data-cast-from="csv" data-paging-next data-paging-desc data-offset="{{router.params.offset}}" data-total="{{project-members.total}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
</div>
@ -216,7 +214,7 @@
View as JSON
</button>
</li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{team.$createdAt|dateText}}"></span></li>
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> Created: <span data-ls-bind="{{team.$createdAt|date}}"></span></li>
</ul>
<form name="teams.delete" class="margin-bottom"

View file

@ -183,7 +183,7 @@
<div class="text-align-center">
<img src="" data-ls-attrs="src={{user|avatar}}" data-size="200" alt="User Avatar" class="avatar huge margin-top-negative-xxl" />
<div class="margin-top-small margin-bottom-small" data-ls-bind="Member since {{user.registration|dateText}}"></div>
<div class="margin-top-small margin-bottom-small" data-ls-bind="Member since {{user.registration|date}}"></div>
<hr class="margin-top-tiny margin-bottom-tiny" data-ls-if="{{user.email}}">
<div class="margin-top-small margin-bottom-small clear" data-ls-if="{{user.email}}">
<span data-ls-bind="{{user.email}}" class="pull-start"></span>

View file

@ -18,8 +18,8 @@
data-scope="console"
data-event="submit"
data-success="alert,redirect"
data-success-param-alert="Password Reset Completed"
data-success-param-url="/auth/signin"
data-success-param-alert-text="Password Reset Completed"
data-success-param-redirect-url="/auth/signin"
data-failure="alert"
data-failure-param-alert-text="Password Reset Failed"
data-failure-param-alert-classname="error">

View file

@ -147,10 +147,11 @@ services:
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@ -514,8 +515,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -545,13 +546,16 @@ services:
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
appwrite-usage-timeseries:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: usage
container_name: appwrite-usage
entrypoint:
- usage
- --type=timeseries
container_name: appwrite-usage-timeseries
<<: *x-logging
restart: unless-stopped
networks:
@ -569,7 +573,40 @@ services:
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-usage-database:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint:
- usage
- --type=database
container_name: appwrite-usage-database
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -611,7 +648,7 @@ services:
command: 'mysqld --innodb-flush-method=fsync'
redis:
image: redis:6.2-alpine
image: redis:7.0.4-alpine
container_name: appwrite-redis
<<: *x-logging
restart: unless-stopped

View file

@ -6,12 +6,16 @@ use Appwrite\Resque\Worker;
use Appwrite\Utopia\Response\Model\Deployment;
use Cron\CronExpression;
use Executor\Executor;
use Utopia\Database\Validator\Authorization;
use Appwrite\Usage\Stats;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\ID;
use Utopia\Storage\Storage;
use Utopia\Database\Document;
use Utopia\Config\Config;
use Utopia\Database\Query;
require_once __DIR__ . '/../init.php';
@ -75,14 +79,12 @@ class BuildsV1 extends Worker
}
$buildId = $deployment->getAttribute('buildId', '');
$build = null;
$startTime = \time();
$startTime = DateTime::now();
if (empty($buildId)) {
$buildId = $dbForProject->getId();
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$read' => [],
'$write' => [],
'$permissions' => [],
'startTime' => $startTime,
'deploymentId' => $deployment->getId(),
'status' => 'processing',
@ -92,7 +94,7 @@ class BuildsV1 extends Worker
'sourceType' => App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL),
'stdout' => '',
'stderr' => '',
'endTime' => 0,
'endTime' => null,
'duration' => 0
]));
$deployment->setAttribute('buildId', $buildId);
@ -143,7 +145,12 @@ class BuildsV1 extends Worker
);
$source = $deployment->getAttribute('path');
$vars = $function->getAttribute('vars', []);
$vars = array_reduce($function['vars'] ?? [], function (array $carry, Document $var) {
$carry[$var->getAttribute('key')] = $var->getAttribute('value');
return $carry;
}, []);
$baseImage = $runtime['image'];
try {
@ -184,13 +191,14 @@ class BuildsV1 extends Worker
/** Update function schedule */
$schedule = $function->getAttribute('schedule', '');
$cron = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null;
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0;
$function->setAttribute('scheduleNext', (int)$next);
$next = (empty($function->getAttribute('deployment')) && !empty($schedule)) ? DateTime::format($cron->getNextRunDate()) : null;
$function->setAttribute('scheduleNext', $next);
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
} catch (\Throwable $th) {
$endtime = \time();
$endtime = DateTime::now();
$interval = (new \DateTime($endtime))->diff(new \DateTime($startTime));
$build->setAttribute('endTime', $endtime);
$build->setAttribute('duration', $endtime - $startTime);
$build->setAttribute('duration', $interval->format('%s'));
$build->setAttribute('status', 'failed');
$build->setAttribute('stderr', $th->getMessage());
Console::error($th->getMessage());
@ -213,6 +221,22 @@ class BuildsV1 extends Worker
channels: $target['channels'],
roles: $target['roles']
);
/** Update usage stats */
global $register;
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
$statsd = $register->get('statsd');
$usage = new Stats($statsd);
$usage
->setParam('projectId', $project->getId())
->setParam('functionId', $function->getId())
->setParam('builds.{scope}.compute', 1)
->setParam('buildStatus', $build->getAttribute('status', ''))
->setParam('buildTime', $build->getAttribute('duration'))
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();
}
}
}

View file

@ -7,8 +7,8 @@ use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
require_once __DIR__ . '/../init.php';
@ -73,7 +73,7 @@ class CertificatesV1 extends Worker
$domain = new Domain($document->getAttribute('domain', ''));
// Get current certificate
$certificate = $this->dbForConsole->findOne('certificates', [new Query('domain', Query::TYPE_EQUAL, [$domain->get()])]);
$certificate = $this->dbForConsole->findOne('certificates', [Query::equal('domain', [$domain->get()])]);
// If we don't have certificate for domain yet, let's create new document. At the end we save it
if (!$certificate) {
@ -116,7 +116,7 @@ class CertificatesV1 extends Worker
// Update certificate info stored in database
$certificate->setAttribute('renewDate', $this->getRenewDate($domain->get()));
$certificate->setAttribute('attempts', 0);
$certificate->setAttribute('issueDate', \time());
$certificate->setAttribute('issueDate', DateTime::now());
} catch (Throwable $e) {
// Set exception as log in certificate document
$certificate->setAttribute('log', $e->getMessage());
@ -129,7 +129,7 @@ class CertificatesV1 extends Worker
$this->notifyError($domain->get(), $e->getMessage(), $attempts);
} finally {
// All actions result in new updatedAt date
$certificate->setAttribute('updated', \time());
$certificate->setAttribute('updated', DateTime::now());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate);
@ -151,7 +151,7 @@ class CertificatesV1 extends Worker
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])]);
$certificateDocument = $this->dbForConsole->findOne('certificates', [Query::equal('domain', [$domain])]);
if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) {
// Merge new data with current data
$certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy()));
@ -176,7 +176,7 @@ class CertificatesV1 extends Worker
if (!empty($envDomain) && $envDomain !== 'localhost') {
return $envDomain;
} else {
$domainDocument = $this->dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']);
$domainDocument = $this->dbForConsole->findOne('domains', [Query::orderAsc('_id')]);
if ($domainDocument) {
return $domainDocument->getAttribute('domain');
}
@ -296,10 +296,9 @@ class CertificatesV1 extends Worker
{
$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;
$validTo = $certData['validTo_time_t'] ?? null;
$dt = (new \DateTime())->setTimestamp($validTo);
return DateTime::addSeconds($dt, -60 * 60 * 24 * 30); // -30 days
}
/**
@ -395,11 +394,12 @@ class CertificatesV1 extends Worker
private function updateDomainDocuments(string $certificateId, string $domain): void
{
$domains = $this->dbForConsole->find('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain])
], 1000);
Query::equal('domain', [$domain]),
Query::limit(1000),
]);
foreach ($domains as $domainDocument) {
$domainDocument->setAttribute('updated', \time());
$domainDocument->setAttribute('updated', DateTime::now());
$domainDocument->setAttribute('certificateId', $certificateId);
$this->dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument);

View file

@ -1,10 +1,11 @@
<?php
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Appwrite\Resque\Worker;
use Executor\Executor;
use Utopia\Storage\Device\Local;
@ -33,7 +34,6 @@ class DeletesV1 extends Worker
{
$project = new Document($this->args['project'] ?? []);
$type = $this->args['type'] ?? '';
switch (strval($type)) {
case DELETE_TYPE_DOCUMENT:
$document = new Document($this->args['document'] ?? []);
@ -70,17 +70,17 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_EXECUTIONS:
$this->deleteExecutionLogs($this->args['timestamp']);
$this->deleteExecutionLogs($this->args['datetime']);
break;
case DELETE_TYPE_AUDIT:
$timestamp = $this->args['timestamp'] ?? 0;
$document = new Document($this->args['document'] ?? []);
if (!empty($timestamp)) {
$this->deleteAuditLogs($this->args['timestamp']);
$datetime = $this->args['datetime'] ?? null;
if (!empty($datetime)) {
$this->deleteAuditLogs($datetime);
}
$document = new Document($this->args['document'] ?? []);
if (!$document->isEmpty()) {
$this->deleteAuditLogsByResource('document/' . $document->getId(), $project->getId());
}
@ -88,15 +88,15 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_ABUSE:
$this->deleteAbuseLogs($this->args['timestamp']);
$this->deleteAbuseLogs($this->args['datetime']);
break;
case DELETE_TYPE_REALTIME:
$this->deleteRealtimeUsage($this->args['timestamp']);
$this->deleteRealtimeUsage($this->args['datetime']);
break;
case DELETE_TYPE_SESSIONS:
$this->deleteExpiredSessions($this->args['timestamp']);
$this->deleteExpiredSessions($this->args['datetime']);
break;
case DELETE_TYPE_CERTIFICATES:
@ -105,7 +105,14 @@ class DeletesV1 extends Worker
break;
case DELETE_TYPE_USAGE:
$this->deleteUsageStats($this->args['timestamp1d'], $this->args['timestamp30m']);
$this->deleteUsageStats($this->args['dateTime1d'], $this->args['dateTime30m']);
break;
case DELETE_TYPE_CACHE_BY_RESOURCE:
$this->deleteCacheByResource($project->getId());
break;
case DELETE_TYPE_CACHE_BY_TIMESTAMP:
$this->deleteCacheByDate();
break;
default:
Console::error('No delete operation for type: ' . $type);
@ -117,6 +124,49 @@ class DeletesV1 extends Worker
{
}
/**
* @param string $projectId
*/
protected function deleteCacheByResource(string $projectId): void
{
$this->deleteCacheFiles([
Query::equal('resource', [$this->args['resource']]),
]);
}
protected function deleteCacheByDate(): void
{
$this->deleteCacheFiles([
Query::lessThan('accessedAt', $this->args['datetime']),
]);
}
protected function deleteCacheFiles($query): void
{
$this->deleteForProjectIds(function (string $projectId) use ($query) {
$dbForProject = $this->getProjectDB($projectId);
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
);
$this->deleteByGroup(
'cache',
$query,
$dbForProject,
function (Document $document) use ($cache, $projectId) {
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
if ($cache->purge($document->getId())) {
Console::success('Deleting cache file: ' . $path);
} else {
Console::error('Failed to delete cache file: ' . $path);
}
}
);
});
}
/**
* @param Document $document database document
* @param string $projectId
@ -150,33 +200,33 @@ class DeletesV1 extends Worker
$dbForProject->deleteCollection('database_' . $databaseId . '_collection_' . $document->getInternalId());
$this->deleteByGroup('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$collectionId])
Query::equal('collectionId', [$collectionId])
], $dbForProject);
$this->deleteByGroup('indexes', [
new Query('collectionId', Query::TYPE_EQUAL, [$collectionId])
Query::equal('collectionId', [$collectionId])
], $dbForProject);
$this->deleteAuditLogsByResource('collection/' . $collectionId, $projectId);
}
/**
* @param int $timestamp1d
* @param int $timestamp30m
* @param string $datetime1d
* @param string $datetime30m
*/
protected function deleteUsageStats(int $timestamp1d, int $timestamp30m)
protected function deleteUsageStats(string $datetime1d, string $datetime30m)
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp1d, $timestamp30m) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime1d, $datetime30m) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Usage stats
$this->deleteByGroup('stats', [
new Query('time', Query::TYPE_LESSER, [$timestamp1d]),
new Query('period', Query::TYPE_EQUAL, ['1d']),
Query::lessThan('time', $datetime1d),
Query::equal('period', ['1d']),
], $dbForProject);
$this->deleteByGroup('stats', [
new Query('time', Query::TYPE_LESSER, [$timestamp30m]),
new Query('period', Query::TYPE_EQUAL, ['30m']),
Query::lessThan('time', $datetime30m),
Query::equal('period', ['30m']),
], $dbForProject);
});
}
@ -191,7 +241,7 @@ class DeletesV1 extends Worker
// Delete Memberships
$this->deleteByGroup('memberships', [
new Query('teamId', Query::TYPE_EQUAL, [$teamId])
Query::equal('teamId', [$teamId])
], $this->getProjectDB($projectId));
}
@ -223,14 +273,14 @@ class DeletesV1 extends Worker
// 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])
Query::equal('userId', [$userId])
], $this->getProjectDB($projectId));
$this->getProjectDB($projectId)->deleteCachedDocument('users', $userId);
// Delete Memberships and decrement team membership counts
$this->deleteByGroup('memberships', [
new Query('userId', Query::TYPE_EQUAL, [$userId])
Query::equal('userId', [$userId])
], $this->getProjectDB($projectId), function (Document $document) use ($projectId) {
if ($document->getAttribute('confirm')) { // Count only confirmed members
@ -251,67 +301,67 @@ class DeletesV1 extends Worker
// Delete tokens
$this->deleteByGroup('tokens', [
new Query('userId', Query::TYPE_EQUAL, [$userId])
Query::equal('userId', [$userId])
], $this->getProjectDB($projectId));
}
/**
* @param int $timestamp
* @param string $datetime
*/
protected function deleteExecutionLogs(int $timestamp): void
protected function deleteExecutionLogs(string $datetime): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Executions
$this->deleteByGroup('executions', [
new Query('$createdAt', Query::TYPE_LESSER, [$timestamp])
Query::lessThan('$createdAt', $datetime)
], $dbForProject);
});
}
/**
* @param int $timestamp
* @param string $datetime
*/
protected function deleteExpiredSessions(int $timestamp): void
protected function deleteExpiredSessions(string $datetime): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Sessions
$this->deleteByGroup('sessions', [
new Query('expire', Query::TYPE_LESSER, [$timestamp])
Query::lessThan('expire', $datetime)
], $dbForProject);
});
}
/**
* @param int $timestamp
* @param string $datetime
*/
protected function deleteRealtimeUsage(int $timestamp): void
protected function deleteRealtimeUsage(string $datetime): void
{
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
$dbForProject = $this->getProjectDB($projectId);
// Delete Dead Realtime Logs
$this->deleteByGroup('realtime', [
new Query('timestamp', Query::TYPE_LESSER, [$timestamp])
Query::lessThan('timestamp', $datetime)
], $dbForProject);
});
}
/**
* @param int $timestamp
* @param string $datetime
* @throws Exception
*/
protected function deleteAbuseLogs(int $timestamp): void
protected function deleteAbuseLogs(string $datetime): void
{
if ($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
if (empty($datetime)) {
throw new Exception('Failed to delete audit logs. No datetime provided');
}
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
$dbForProject = $this->getProjectDB($projectId);
$timeLimit = new TimeLimit("", 0, 1, $dbForProject);
$abuse = new Abuse($timeLimit);
$status = $abuse->cleanup($timestamp);
$status = $abuse->cleanup($datetime);
if (!$status) {
throw new Exception('Failed to delete Abuse logs for project ' . $projectId);
}
@ -319,17 +369,19 @@ class DeletesV1 extends Worker
}
/**
* @param int $timestamp
* @param string $datetime
* @throws Exception
*/
protected function deleteAuditLogs(int $timestamp): void
protected function deleteAuditLogs(string $datetime): void
{
if ($timestamp == 0) {
throw new Exception('Failed to delete audit logs. No timestamp provided');
if (empty($datetime)) {
throw new Exception('Failed to delete audit logs. No datetime provided');
}
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$this->deleteForProjectIds(function (string $projectId) use ($datetime) {
$dbForProject = $this->getProjectDB($projectId);
$audit = new Audit($dbForProject);
$status = $audit->cleanup($timestamp);
$status = $audit->cleanup($datetime);
if (!$status) {
throw new Exception('Failed to delete Audit logs for project' . $projectId);
}
@ -337,14 +389,15 @@ class DeletesV1 extends Worker
}
/**
* @param int $timestamp
* @param string $resource
* @param string $projectId
*/
protected function deleteAuditLogsByResource(string $resource, string $projectId): void
{
$dbForProject = $this->getProjectDB($projectId);
$this->deleteByGroup(Audit::COLLECTION, [
new Query('resource', Query::TYPE_EQUAL, [$resource])
Query::equal('resource', [$resource])
], $dbForProject);
}
@ -364,7 +417,7 @@ class DeletesV1 extends Worker
$storageFunctions = new Local(APP_STORAGE_FUNCTIONS . '/app-' . $projectId);
$deploymentIds = [];
$this->deleteByGroup('deployments', [
new Query('resourceId', Query::TYPE_EQUAL, [$functionId])
Query::equal('resourceId', [$functionId])
], $dbForProject, function (Document $document) use ($storageFunctions, &$deploymentIds) {
$deploymentIds[] = $document->getId();
if ($storageFunctions->delete($document->getAttribute('path', ''), true)) {
@ -381,7 +434,7 @@ class DeletesV1 extends Worker
$storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId);
foreach ($deploymentIds as $deploymentId) {
$this->deleteByGroup('builds', [
new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId])
Query::equal('deploymentId', [$deploymentId])
], $dbForProject, function (Document $document) use ($storageBuilds, $deploymentId) {
if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) {
Console::success('Deleted build files: ' . $document->getAttribute('outputPath', ''));
@ -396,7 +449,7 @@ class DeletesV1 extends Worker
*/
Console::info("Deleting executions for function " . $functionId);
$this->deleteByGroup('executions', [
new Query('functionId', Query::TYPE_EQUAL, [$functionId])
Query::equal('functionId', [$functionId])
], $dbForProject);
/**
@ -440,7 +493,7 @@ class DeletesV1 extends Worker
Console::info("Deleting builds for deployment " . $deploymentId);
$storageBuilds = new Local(APP_STORAGE_BUILDS . '/app-' . $projectId);
$this->deleteByGroup('builds', [
new Query('deploymentId', Query::TYPE_EQUAL, [$deploymentId])
Query::equal('deploymentId', [$deploymentId])
], $dbForProject, function (Document $document) use ($storageBuilds) {
if ($storageBuilds->delete($document->getAttribute('outputPath', ''), true)) {
Console::success('Deleted build files: ' . $document->getAttribute('outputPath', ''));
@ -499,7 +552,7 @@ class DeletesV1 extends Worker
$executionStart = \microtime(true);
while ($sum === $limit) {
$projects = $this->getConsoleDB()->find('projects', [], $limit, ($chunk * $limit));
$projects = $this->getConsoleDB()->find('projects', [Query::limit($limit), Query::offset($chunk * $limit)]);
$chunk++;
@ -538,7 +591,7 @@ class DeletesV1 extends Worker
while ($sum === $limit) {
$chunk++;
$results = $database->find($collection, $queries, $limit, 0);
$results = $database->find($collection, \array_merge([Query::limit($limit)], $queries));
$sum = count($results);
@ -565,7 +618,7 @@ class DeletesV1 extends Worker
// If domain has certificate generated
if (isset($document['certificateId'])) {
$domainUsingCertificate = $consoleDB->findOne('domains', [
new Query('certificateId', Query::TYPE_EQUAL, [$document['certificateId']])
Query::equal('certificateId', [$document['certificateId']])
]);
if (!$domainUsingCertificate) {

View file

@ -4,7 +4,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Appwrite\Stats\Stats;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\Response\Model\Execution;
use Cron\CronExpression;
use Executor\Executor;
@ -12,8 +12,12 @@ use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
require_once __DIR__ . '/../init.php';
@ -61,7 +65,11 @@ class FunctionsV1 extends Worker
/** @var Document[] $functions */
while ($sum >= $limit) {
$functions = $database->find('functions', [], $limit, $offset, ['name'], [Database::ORDER_ASC]);
$functions = $database->find('functions', [
Query::limit($limit),
Query::offset($offset),
Query::orderAsc('name'),
]);
$sum = \count($functions);
$offset = $offset + $limit;
@ -147,31 +155,26 @@ class FunctionsV1 extends Worker
}
$cron = new CronExpression($function->getAttribute('schedule'));
$next = (int) $cron->getNextRunDate()->format('U');
$next = DateTime::format($cron->getNextRunDate());
$function
->setAttribute('scheduleNext', $next)
->setAttribute('schedulePrevious', \time());
->setAttribute('schedulePrevious', DateTime::now());
$function = $database->updateDocument(
'functions',
$function->getId(),
$function->setAttribute('scheduleNext', (int) $next)
$function->setAttribute('scheduleNext', $next)
);
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);
->setProject($project)
->schedule(new \DateTime($next));
;
$this->execute(
project: $project,
@ -234,11 +237,10 @@ class FunctionsV1 extends Worker
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
$executionId = $dbForProject->getId();
$executionId = ID::unique();
$execution = $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()],
'$write' => [],
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
'functionId' => $functionId,
'deploymentId' => $deploymentId,
'trigger' => $trigger,
@ -257,8 +259,13 @@ class FunctionsV1 extends Worker
$execution->setAttribute('status', 'processing');
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
$vars = array_reduce($function['vars'] ?? [], function (array $carry, Document $var) {
$carry[$var->getAttribute('key')] = $var->getAttribute('value');
return $carry;
}, []);
/** Collect environment variables */
$vars = [
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_ID' => $functionId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deploymentId,
@ -271,8 +278,7 @@ class FunctionsV1 extends Worker
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_USER_ID' => $user->getId(),
'APPWRITE_FUNCTION_JWT' => $jwt,
];
$vars = \array_merge($function->getAttribute('vars', []), $vars);
]);
/** Execute function */
try {
@ -293,13 +299,13 @@ class FunctionsV1 extends Worker
->setAttribute('status', $executionResponse['status'])
->setAttribute('statusCode', $executionResponse['statusCode'])
->setAttribute('response', $executionResponse['response'])
->setAttribute('stdout', $executionResponse['stdout'])
->setAttribute('stderr', $executionResponse['stderr'])
->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
$endtime = \microtime(true);
$time = $endtime - $execution->getCreatedAt();
$interval = (new \DateTime())->diff(new \DateTime($execution->getCreatedAt()));
$execution
->setAttribute('time', $time)
->setAttribute('time', (float)$interval->format('%s.%f'))
->setAttribute('status', 'failed')
->setAttribute('statusCode', $th->getCode())
->setAttribute('stderr', $th->getMessage());
@ -359,9 +365,9 @@ class FunctionsV1 extends Worker
$usage
->setParam('projectId', $project->getId())
->setParam('functionId', $function->getId())
->setParam('functionExecution', 1)
->setParam('functionStatus', $execution->getAttribute('status', ''))
->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000) // ms
->setParam('executions.{scope}.compute', 1)
->setParam('executionStatus', $execution->getAttribute('status', ''))
->setParam('executionTime', $execution->getAttribute('time'))
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->submit();

View file

@ -1,12 +1,12 @@
<?php
use Appwrite\Auth\Phone;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\Auth\SMS;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Resque\Worker;
use Utopia\App;
@ -19,7 +19,7 @@ Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
class MessagingV1 extends Worker
{
protected ?Phone $phone = null;
protected ?SMS $sms = null;
protected ?string $from = null;
public function getName(): string
@ -29,11 +29,11 @@ class MessagingV1 extends Worker
public function init(): void
{
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
$this->phone = match ($dsn->getHost()) {
$this->sms = match ($dsn->getHost()) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
@ -43,12 +43,12 @@ class MessagingV1 extends Worker
default => null
};
$this->from = App::getEnv('_APP_PHONE_FROM');
$this->from = App::getEnv('_APP_SMS_FROM');
}
public function run(): void
{
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::info('Skipped sms processing. No Phone provider has been set.');
return;
}
@ -62,7 +62,7 @@ class MessagingV1 extends Worker
$message = $this->args['message'];
try {
$this->phone->send($this->from, $recipient, $message);
$this->sms->send($this->from, $recipient, $message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}

View file

@ -23,6 +23,7 @@
"autoload-dev": {
"psr-4": {
"Tests\\E2E\\": "tests/e2e",
"Tests\\Unit\\": "tests/unit",
"Appwrite\\Tests\\": "tests/extensions"
}
},
@ -41,23 +42,23 @@
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.10.*",
"utopia-php/framework": "0.20.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/framework": "0.21.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.7.*",
"utopia-php/abuse": "0.12.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.8.*",
"utopia-php/audit": "0.13.*",
"utopia-php/cache": "0.6.*",
"utopia-php/platform": "dev-dev",
"utopia-php/platform": "dev-feat-upgrade-framework",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.18.*",
"utopia-php/database": "0.24.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/domains": "1.1.*",
"utopia-php/swoole": "0.3.*",
"utopia-php/storage": "0.9.*",
"utopia-php/storage": "0.11.*",
"utopia-php/websocket": "0.1.0",
"utopia-php/image": "0.5.*",
"utopia-php/orchestration": "0.6.*",
@ -81,7 +82,8 @@
}
],
"require-dev": {
"appwrite/sdk-generator": "0.19.5",
"appwrite/sdk-generator": "0.22.0",
"ext-fileinfo": "*",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.6",
"swoole/ide-helper": "4.8.9",

226
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4f3868304dc2774906290ca8d36b0d06",
"content-hash": "c45d79c5b91ed06a4a4a93863da93eff",
"packages": [
{
"name": "adhocore/jwt",
@ -115,11 +115,11 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.10.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "09874846c6bdb7be58c97b12323d2b35ec995409"
"reference": "547fc026e11c0946846a8ac690898f5bf53be101"
},
"require": {
"php": ">=8.0",
@ -154,7 +154,7 @@
"php",
"runtimes"
],
"time": "2022-06-28T05:26:20+00:00"
"time": "2022-08-15T14:03:36+00:00"
},
{
"name": "chillerlan/php-qrcode",
@ -481,16 +481,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.5",
"version": "7.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82"
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"shasum": ""
},
"require": {
@ -505,10 +505,10 @@
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"ext-curl": "*",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"phpunit/phpunit": "^8.5.29 || ^9.5.23",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@ -518,8 +518,12 @@
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "7.4-dev"
"dev-master": "7.5-dev"
}
},
"autoload": {
@ -585,7 +589,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.5"
"source": "https://github.com/guzzle/guzzle/tree/7.5.0"
},
"funding": [
{
@ -601,20 +605,20 @@
"type": "tidelift"
}
],
"time": "2022-06-20T22:16:13+00:00"
"time": "2022-08-28T15:39:27+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.5.1",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da"
"reference": "b94b2807d85443f9719887892882d0329d1e2598"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
"url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598",
"reference": "b94b2807d85443f9719887892882d0329d1e2598",
"shasum": ""
},
"require": {
@ -669,7 +673,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.5.1"
"source": "https://github.com/guzzle/promises/tree/1.5.2"
},
"funding": [
{
@ -685,20 +689,20 @@
"type": "tidelift"
}
],
"time": "2021-10-22T20:56:57+00:00"
"time": "2022-08-28T14:55:35+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.4.0",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "13388f00956b1503577598873fffb5ae994b5737"
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737",
"reference": "13388f00956b1503577598873fffb5ae994b5737",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379",
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379",
"shasum": ""
},
"require": {
@ -712,15 +716,19 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.8 || ^9.3.10"
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "2.4-dev"
}
@ -784,7 +792,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.4.0"
"source": "https://github.com/guzzle/psr7/tree/2.4.1"
},
"funding": [
{
@ -800,7 +808,7 @@
"type": "tidelift"
}
],
"time": "2022-06-20T21:43:11+00:00"
"time": "2022-08-28T14:45:39+00:00"
},
{
"name": "influxdb/influxdb-php",
@ -1733,22 +1741,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.7.0",
"version": "0.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "52fb20e39e2e9619948bc0a73b52e10caa71350d"
"reference": "aa1e1aae163ecf8ea81d48857ff55c241dcb695f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/52fb20e39e2e9619948bc0a73b52e10caa71350d",
"reference": "52fb20e39e2e9619948bc0a73b52e10caa71350d",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/aa1e1aae163ecf8ea81d48857ff55c241dcb695f",
"reference": "aa1e1aae163ecf8ea81d48857ff55c241dcb695f",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": ">=0.11 <1.0"
"utopia-php/database": "0.24.0"
},
"require-dev": {
"phpunit/phpunit": "^9.4",
@ -1780,9 +1789,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.7.0"
"source": "https://github.com/utopia-php/abuse/tree/0.12.0"
},
"time": "2021-12-27T13:06:45+00:00"
"time": "2022-08-27T09:50:09+00:00"
},
{
"name": "utopia-php/analytics",
@ -1841,22 +1850,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.8.0",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "b46dc42614a69437c45eb229249b6a6d000122c1"
"reference": "a2f30ccfba7a61b1718b9ebd4557ed0d8a4dcb5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/b46dc42614a69437c45eb229249b6a6d000122c1",
"reference": "b46dc42614a69437c45eb229249b6a6d000122c1",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/a2f30ccfba7a61b1718b9ebd4557ed0d8a4dcb5b",
"reference": "a2f30ccfba7a61b1718b9ebd4557ed0d8a4dcb5b",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": ">=0.11 <1.0"
"utopia-php/database": "0.24.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -1888,22 +1897,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.8.0"
"source": "https://github.com/utopia-php/audit/tree/0.13.0"
},
"time": "2021-12-27T13:05:56+00:00"
"time": "2022-08-27T09:18:57+00:00"
},
{
"name": "utopia-php/cache",
"version": "0.6.0",
"version": "0.6.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
"reference": "8ea1353a4bbab617e23c865a7c97b60d8074aee3"
"reference": "9889235a6d3da6cbb1f435201529da4d27c30e79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/8ea1353a4bbab617e23c865a7c97b60d8074aee3",
"reference": "8ea1353a4bbab617e23c865a7c97b60d8074aee3",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/9889235a6d3da6cbb1f435201529da4d27c30e79",
"reference": "9889235a6d3da6cbb1f435201529da4d27c30e79",
"shasum": ""
},
"require": {
@ -1941,9 +1950,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
"source": "https://github.com/utopia-php/cache/tree/0.6.0"
"source": "https://github.com/utopia-php/cache/tree/0.6.1"
},
"time": "2022-04-04T12:30:05+00:00"
"time": "2022-08-10T08:12:46+00:00"
},
{
"name": "utopia-php/cli",
@ -2051,16 +2060,16 @@
},
{
"name": "utopia-php/database",
"version": "0.18.9",
"version": "0.24.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "227b3ca919149b7b0d6556c8effe9ee46ed081e6"
"reference": "7da841d65d87e9f2c242589e58c38880def44dd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/227b3ca919149b7b0d6556c8effe9ee46ed081e6",
"reference": "227b3ca919149b7b0d6556c8effe9ee46ed081e6",
"url": "https://api.github.com/repos/utopia-php/database/zipball/7da841d65d87e9f2c242589e58c38880def44dd8",
"reference": "7da841d65d87e9f2c242589e58c38880def44dd8",
"shasum": ""
},
"require": {
@ -2109,9 +2118,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.18.9"
"source": "https://github.com/utopia-php/database/tree/0.24.0"
},
"time": "2022-07-19T09:42:53+00:00"
"time": "2022-08-27T09:16:05+00:00"
},
{
"name": "utopia-php/domains",
@ -2169,16 +2178,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.20.0",
"version": "0.21.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/framework.git",
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a"
"reference": "5aa5431788460a782065e42b0e8a35e7f139af2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/5aa5431788460a782065e42b0e8a35e7f139af2f",
"reference": "5aa5431788460a782065e42b0e8a35e7f139af2f",
"shasum": ""
},
"require": {
@ -2212,9 +2221,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/framework/issues",
"source": "https://github.com/utopia-php/framework/tree/0.20.0"
"source": "https://github.com/utopia-php/framework/tree/0.21.0"
},
"time": "2022-07-30T09:55:28+00:00"
"time": "2022-08-12T11:37:21+00:00"
},
{
"name": "utopia-php/image",
@ -2442,18 +2451,18 @@
},
{
"name": "utopia-php/platform",
"version": "dev-dev",
"version": "dev-feat-upgrade-framework",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "cc41247d0636648ba854afba8ebf68be441cfd1b"
"reference": "4c2fcb8b9f046948ab812552e4e448057efef936"
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/cli": "0.13.*",
"utopia-php/framework": "0.20.*"
"utopia-php/framework": "0.21.*"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -2498,7 +2507,7 @@
"upf",
"utopia"
],
"time": "2022-08-03T04:41:22+00:00"
"time": "2022-09-01T11:59:42+00:00"
},
{
"name": "utopia-php/preloader",
@ -2607,19 +2616,22 @@
},
{
"name": "utopia-php/storage",
"version": "0.9.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "c7912481a56e17cc86358fa8de57309de5e88ef7"
"reference": "59802cf281d1976560cf6e353f250a9b870efddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/c7912481a56e17cc86358fa8de57309de5e88ef7",
"reference": "c7912481a56e17cc86358fa8de57309de5e88ef7",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/59802cf281d1976560cf6e353f250a9b870efddc",
"reference": "59802cf281d1976560cf6e353f250a9b870efddc",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"ext-zlib": "*",
"ext-zstd": "*",
"php": ">=8.0",
"utopia-php/framework": "0.*.*"
},
@ -2653,9 +2665,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/storage/issues",
"source": "https://github.com/utopia-php/storage/tree/0.9.0"
"source": "https://github.com/utopia-php/storage/tree/0.11.0"
},
"time": "2022-05-19T11:05:45+00:00"
"time": "2022-08-31T09:17:31+00:00"
},
{
"name": "utopia-php/swoole",
@ -2888,29 +2900,29 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.19.5",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "04de540cf683e2b08b3192c137dde7f2c37003d9"
"reference": "805d8ff91656e8bc178a5461b17ee813f30de737"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/04de540cf683e2b08b3192c137dde7f2c37003d9",
"reference": "04de540cf683e2b08b3192c137dde7f2c37003d9",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/805d8ff91656e8bc178a5461b17ee813f30de737",
"reference": "805d8ff91656e8bc178a5461b17ee813f30de737",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"matthiasmullie/minify": "^1.3",
"matthiasmullie/minify": "^1.3.68",
"php": ">=7.0.0",
"twig/twig": "^3.3"
"twig/twig": "^3.4.1"
},
"require-dev": {
"brianium/paratest": "^6.4",
"phpunit/phpunit": "^9.5.13"
"phpunit/phpunit": "^9.5.21"
},
"type": "library",
"autoload": {
@ -2932,9 +2944,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.19.5"
"source": "https://github.com/appwrite/sdk-generator/tree/0.22.0"
},
"time": "2022-07-06T11:05:57+00:00"
"time": "2022-09-01T10:48:13+00:00"
},
{
"name": "doctrine/instantiator",
@ -3008,16 +3020,16 @@
},
{
"name": "matthiasmullie/minify",
"version": "1.3.68",
"version": "1.3.69",
"source": {
"type": "git",
"url": "https://github.com/matthiasmullie/minify.git",
"reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297"
"reference": "a61c949cccd086808063611ef9698eabe42ef22f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297",
"reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/a61c949cccd086808063611ef9698eabe42ef22f",
"reference": "a61c949cccd086808063611ef9698eabe42ef22f",
"shasum": ""
},
"require": {
@ -3066,7 +3078,7 @@
],
"support": {
"issues": "https://github.com/matthiasmullie/minify/issues",
"source": "https://github.com/matthiasmullie/minify/tree/1.3.68"
"source": "https://github.com/matthiasmullie/minify/tree/1.3.69"
},
"funding": [
{
@ -3074,7 +3086,7 @@
"type": "github"
}
],
"time": "2022-04-19T08:28:56+00:00"
"time": "2022-08-01T09:00:18+00:00"
},
{
"name": "matthiasmullie/path-converter",
@ -3584,23 +3596,23 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.15",
"version": "9.2.17",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
"reference": "aa94dc41e8661fe90c7316849907cba3007b10d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8",
"reference": "aa94dc41e8661fe90c7316849907cba3007b10d8",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0",
"nikic/php-parser": "^4.14",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -3649,7 +3661,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17"
},
"funding": [
{
@ -3657,7 +3669,7 @@
"type": "github"
}
],
"time": "2022-03-07T09:28:20+00:00"
"time": "2022-08-30T12:24:04+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -4860,16 +4872,16 @@
},
{
"name": "sebastian/type",
"version": "3.0.0",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad"
"reference": "fb44e1cc6e557418387ad815780360057e40753e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb44e1cc6e557418387ad815780360057e40753e",
"reference": "fb44e1cc6e557418387ad815780360057e40753e",
"shasum": ""
},
"require": {
@ -4881,7 +4893,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.1-dev"
}
},
"autoload": {
@ -4904,7 +4916,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/3.0.0"
"source": "https://github.com/sebastianbergmann/type/tree/3.1.0"
},
"funding": [
{
@ -4912,7 +4924,7 @@
"type": "github"
}
],
"time": "2022-03-15T09:54:48+00:00"
"time": "2022-08-29T06:55:37+00:00"
},
{
"name": "sebastian/version",
@ -5331,16 +5343,16 @@
},
{
"name": "twig/twig",
"version": "v3.4.1",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342"
"reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e939eae92386b69b49cfa4599dd9bead6bf4a342",
"reference": "e939eae92386b69b49cfa4599dd9bead6bf4a342",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077",
"reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077",
"shasum": ""
},
"require": {
@ -5391,7 +5403,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.4.1"
"source": "https://github.com/twigphp/Twig/tree/v3.4.2"
},
"funding": [
{
@ -5403,7 +5415,7 @@
"type": "tidelift"
}
],
"time": "2022-05-17T05:48:52+00:00"
"time": "2022-08-12T06:47:24+00:00"
}
],
"aliases": [],
@ -5428,9 +5440,11 @@
"ext-zlib": "*",
"ext-sockets": "*"
},
"platform-dev": [],
"platform-dev": {
"ext-fileinfo": "*"
},
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.2.0"
}

View file

@ -103,11 +103,9 @@ services:
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
# - ./vendor/utopia/database:/usr/src/code/vendor/utopia/database
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
# - ./debug:/tmp
- ./dev:/usr/local/dev
depends_on:
- mariadb
@ -173,10 +171,11 @@ services:
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
entrypoint: realtime
@ -207,7 +206,6 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# - ./vendor:/usr/src/code/vendor
depends_on:
- mariadb
- redis
@ -330,7 +328,7 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
depends_on:
- redis
- mariadb
@ -545,8 +543,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -561,6 +559,7 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
depends_on:
- redis
environment:
@ -579,13 +578,16 @@ services:
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
appwrite-usage:
entrypoint: usage
appwrite-usage-timeseries:
entrypoint:
- usage
- --type=timeseries
<<: *x-logging
container_name: appwrite-usage
container_name: appwrite-usage-timeseries
build:
context: .
args:
@ -609,7 +611,46 @@ services:
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
appwrite-usage-database:
entrypoint:
- usage
- --type=database
<<: *x-logging
container_name: appwrite-usage-database
build:
context: .
args:
- DEBUG=false
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./dev:/usr/local/dev
depends_on:
- influxdb
- mariadb
environment:
- _APP_ENV
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_TIMESERIES_INTERVAL
- _APP_USAGE_DATABASE_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@ -668,7 +709,7 @@ services:
# - SMARTHOST_PORT=587
redis:
image: redis:6.2-alpine
image: redis:7.0.4-alpine
<<: *x-logging
container_name: appwrite-redis
command: >

View file

@ -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());
}
}
});
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createEmailSession(
"email@example.com",
"password"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createJWT(new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
});
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createMagicURLSession(
"[USER_ID]",
"email@example.com",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createOAuth2Session(
this,
"amazon",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createPhoneSession(
"[USER_ID]",
""
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,46 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createPhoneVerification(new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
});
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createRecovery(
"email@example.com",
"https://example.com"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createVerification(
"https://example.com"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,50 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.create(
"[USER_ID]",
"email@example.com",
"password",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.deleteSession(
"[SESSION_ID]"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -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.deleteSessions(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());
}
}
});
}
}

View file

@ -0,0 +1,47 @@
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.getLogs(
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());
}
}
}
);
}
}

View file

@ -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.getPrefs(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());
}
}
});
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.getSession(
"[SESSION_ID]"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -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.getSessions(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());
}
}
});
}
}

View file

@ -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.get(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());
}
}
});
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateEmail(
"email@example.com",
"password"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateMagicURLSession(
"[USER_ID]",
"[SECRET]"
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());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateName(
"[NAME]"
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());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updatePassword(
"password",
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updatePhoneSession(
"[USER_ID]",
"[SECRET]"
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());
}
}
}
);
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updatePhoneVerification(
"[USER_ID]",
"[SECRET]"
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());
}
}
}
);
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updatePhone(
"",
"password"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updatePrefs(
mapOf( "a" to "b" )
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());
}
}
}
);
}
}

View file

@ -0,0 +1,51 @@
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.updateRecovery(
"[USER_ID]",
"[SECRET]",
"password",
"password"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateSession(
"[SESSION_ID]"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -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.updateStatus(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());
}
}
});
}
}

View file

@ -0,0 +1,49 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Account
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Client client = new Client(getApplicationContext())
.setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateVerification(
"[USER_ID]",
"[SECRET]"
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());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Avatars
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
Avatars avatars = new Avatars(client);
avatars.getBrowser(
"aa",
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());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Avatars
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
Avatars avatars = new Avatars(client);
avatars.getCreditCard(
"amex",
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());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Avatars
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
Avatars avatars = new Avatars(client);
avatars.getFavicon(
"https://example.com"
new Continuation<Object>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
String json = "";
try {
if (o instanceof Result.Failure) {
Result.Failure failure = (Result.Failure) o;
throw failure.exception;
} else {
Response response = (Response) o;
json = response.body().string();
}
} catch (Throwable th) {
Log.e("ERROR", th.toString());
}
}
}
);
}
}

View file

@ -0,0 +1,48 @@
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import io.appwrite.Client
import io.appwrite.services.Avatars
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
Avatars avatars = new Avatars(client);
avatars.getFlag(
"af",
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