1
0
Fork 0
mirror of synced 2024-05-20 12:42:39 +12:00

Merge branch '0.8.x' of github.com:appwrite/appwrite into feat-jwt-support-for-sdks

This commit is contained in:
Eldad Fux 2021-04-16 10:20:12 +03:00
commit 7a0ff7699a
92 changed files with 1654 additions and 510 deletions

3
.env
View file

@ -1,5 +1,6 @@
_APP_ENV=production
_APP_ENV=development
_APP_LOCALE=en
_APP_SYSTEM_EMAIL_NAME=Appwrite
_APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io
@ -16,7 +17,7 @@ _APP_DB_PORT=3306
_APP_DB_SCHEMA=appwrite
_APP_DB_USER=user
_APP_DB_PASS=password
_APP_STORAGE_ANTIVIRUS=enabled
_APP_STORAGE_ANTIVIRUS=disabled
_APP_STORAGE_ANTIVIRUS_HOST=clamav
_APP_STORAGE_ANTIVIRUS_PORT=3310
_APP_INFLUXDB_HOST=influxdb

View file

@ -5,15 +5,28 @@
- Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md), #914)
- Added events for functions and executions (#971)
- Added JWT support
- Splited token & session models to become 2 different internal entities (#922)
- Added Dart 2.12 as a new Cloud Functions runtime (#989)
- ClamAV is now disabled by default to allow lower min requirments for Appwrite (#1064)
- Added a new env var named `_APP_LOCALE` that allow to change the default `en` locale value (#1056)
- Updated all the console bottom control to be consistent. Dropped the `+` icon (#1062)
- Added runtime functions environment for Python 3.9
- Added runtime functions environment for Deno 1.8
## Bugs
- Fixed default value for HTTPS force option
- Fixed form array casting in dashboard
- Fixed collection document rule form in dashboard
## Breaking Changes
## Breaking Changes (Read before upgrading!)
- Rename `deleteuser` to `delete` on Users Api
- Only logged in users can execute functions (for guests, use anonymous login)
- Only the user who has triggered the execution get access to the relevant execution logs
- Function execution env `APPWRITE_FUNCTION_EVENT_PAYLOAD` renamed to `APPWRITE_FUNCTION_EVENT_DATA`
- Introdcues rate limits for:
- Team invite (10 requests in every 60 minutes per IP address)
# Version 0.7.2

View file

@ -68,6 +68,7 @@ ARG VERSION=dev
ENV _APP_SERVER=swoole \
_APP_ENV=production \
_APP_LOCALE=en \
_APP_DOMAIN=localhost \
_APP_DOMAIN_TARGET=localhost \
_APP_HOME=https://appwrite.io \

View file

@ -271,6 +271,16 @@ $collections = [
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Sessions',
'key' => 'sessions',
'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT,
'default' => [],
'required' => false,
'array' => true,
'list' => [Database::SYSTEM_COLLECTION_SESSIONS],
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Tokens',
@ -293,11 +303,11 @@ $collections = [
],
],
],
Database::SYSTEM_COLLECTION_TOKENS => [
Database::SYSTEM_COLLECTION_SESSIONS => [
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
'$id' => Database::SYSTEM_COLLECTION_TOKENS,
'$id' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['*']],
'name' => 'Token',
'name' => 'Session',
'structure' => true,
'rules' => [
[
@ -306,16 +316,34 @@ $collections = [
'key' => 'userId',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => null,
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Provider',
'key' => 'provider',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Provider User Identifier',
'key' => 'providerUid',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => false,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Type',
'key' => 'type',
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
'default' => null,
'required' => true,
'label' => 'Provider Token',
'key' => 'providerToken',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => false,
'array' => false,
],
[
@ -473,6 +501,69 @@ $collections = [
],
],
],
Database::SYSTEM_COLLECTION_TOKENS => [
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
'$id' => Database::SYSTEM_COLLECTION_TOKENS,
'$permissions' => ['read' => ['*']],
'name' => 'Token',
'structure' => true,
'rules' => [
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'User ID',
'key' => 'userId',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => null,
'required' => false,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Type',
'key' => 'type',
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
'default' => null,
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Secret',
'key' => 'secret',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'Expire',
'key' => 'expire',
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
'default' => 0,
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'User Agent',
'key' => 'userAgent',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => true,
'array' => false,
],
[
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'IP',
'key' => 'ip',
'type' => Database::SYSTEM_VAR_TYPE_IP,
'default' => '',
'required' => true,
'array' => false,
],
],
],
Database::SYSTEM_COLLECTION_MEMBERSHIPS => [
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
'$id' => Database::SYSTEM_COLLECTION_MEMBERSHIPS,
@ -1617,26 +1708,6 @@ foreach ($providers as $index => $provider) {
'array' => false,
'filter' => ['encrypt'],
];
$collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'OAuth2 '.\ucfirst($index).' ID',
'key' => 'oauth2'.\ucfirst($index),
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => false,
'array' => false,
];
$collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'label' => 'OAuth2 '.\ucfirst($index).' Access Token',
'key' => 'oauth2'.\ucfirst($index).'AccessToken',
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
'default' => '',
'required' => false,
'array' => false,
];
}
return $collections;

View file

@ -70,6 +70,15 @@ $environments = [
'logo' => 'python.png',
'supports' => [System::X86, System::PPC, System::ARM],
],
'python-3.9' => [
'name' => 'Python',
'version' => '3.9',
'base' => 'python:3.9-alpine',
'image' => 'appwrite/env-python-3.9:1.0.0',
'build' => '/usr/src/code/docker/environments/python-3.9',
'logo' => 'python.png',
'supports' => [System::X86, System::PPC, System::ARM],
],
'deno-1.2' => [
'name' => 'Deno',
'version' => '1.2',
@ -97,6 +106,15 @@ $environments = [
'logo' => 'deno.png',
'supports' => [System::X86, System::PPC, System::ARM],
],
'deno-1.8' => [
'name' => 'Deno',
'version' => '1.8',
'base' => 'hayd/deno:alpine-1.8.2',
'image' => 'appwrite/env-deno-1.8:1.0.0',
'build' => '/usr/src/code/docker/environments/deno-1.8',
'logo' => 'deno.png',
'supports' => [System::X86, System::PPC, System::ARM],
],
'dart-2.10' => [
'name' => 'Dart',
'version' => '2.10',
@ -106,6 +124,15 @@ $environments = [
'logo' => 'dart.png',
'supports' => [System::X86],
],
'dart-2.12' => [
'name' => 'Dart',
'version' => '2.12',
'base' => 'google/dart:2.12',
'image' => 'appwrite/env-dart-2.12:1.0.0',
'build' => '/usr/src/code/docker/environments/dart-2.12',
'logo' => 'dart.png',
'supports' => [System::X86],
],
'dotnet-3.1' => [
'name' => '.NET',
'version' => '3.1',

View file

@ -84,17 +84,17 @@ return [
],
'database.documents.create' => [
'description' => 'This event triggers when a database document is created.',
'model' => Response::MODEL_ANY,
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'database.documents.update' => [
'description' => 'This event triggers when a database document is updated.',
'model' => Response::MODEL_ANY,
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'database.documents.delete' => [
'description' => 'This event triggers when a database document is deleted.',
'model' => Response::MODEL_ANY,
'model' => Response::MODEL_DOCUMENT,
'note' => '',
],
'functions.create' => [

View file

@ -15,6 +15,14 @@ return [
'required' => false,
'question' => '',
],
[
'name' => '_APP_LOCALE',
'description' => 'Set your Appwrite\'s locale. By default, the locale is set to \'en\'.',
'introduction' => '',
'default' => 'en',
'required' => false,
'question' => '',
],
[
'name' => '_APP_OPTIONS_ABUSE',
'description' => 'Allows you to disable abuse checks and API rate limiting. By default, set to \'enabled\'. To cancel the abuse checking, set to \'disabled\'. It is not recommended to disable this check-in a production environment.',
@ -309,9 +317,9 @@ return [
],
[
'name' => '_APP_STORAGE_ANTIVIRUS',
'description' => 'This variable allows you to disable the internal anti-virus scans. This value is set to \'enabled\' by default, to cancel the scans set the value to \'disabled\'. When disabled, it\'s recommended to turn off the ClamAV container for better resource usage.',
'description' => 'This variable allows you to disable the internal anti-virus scans. This value is set to \'disabled\' by default, to enable the scans set the value to \'enabled\'. Before enabling, you must add the ClamAV service and depend on it on main Appwrite service.',
'introduction' => '',
'default' => 'enabled',
'default' => 'disabled',
'required' => false,
'question' => '',
],
@ -381,7 +389,7 @@ return [
'name' => '_APP_FUNCTIONS_ENVS',
'description' => 'This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\n\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\n\nCurrently, supported environments are: ' . \implode(', ', \array_keys(Config::getParam('providers'))),
'introduction' => '0.7.0',
'default' => 'node-14.5,deno-1.6,php-7.4,python-3.8,ruby-3.0,dotnet-5.0',
'default' => 'node-14.5,deno-1.8,php-7.4,python-3.9,ruby-3.0,dotnet-5.0',
'required' => false,
'question' => '',
],

View file

@ -6,10 +6,10 @@ use Utopia\Exception;
use Utopia\Config\Config;
use Utopia\Validator\Assoc;
use Utopia\Validator\Text;
use Utopia\Validator\Email;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Host;
use Utopia\Validator\URL;
use Appwrite\Network\Validator\Host;
use Appwrite\Network\Validator\URL;
use Utopia\Audit\Audit;
use Utopia\Audit\Adapters\MySQL as AuditAdapter;
use Appwrite\Auth\Auth;
@ -190,10 +190,11 @@ App::post('/v1/account/sessions')
$secret = Auth::tokenGenerator();
$session = new Document(array_merge(
[
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]],
'userId' => $profile->getId(),
'type' => Auth::TOKEN_TYPE_LOGIN,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $email,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -210,7 +211,7 @@ App::post('/v1/account/sessions')
throw new Exception('Failed saving session to DB', 500);
}
$profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
$profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
$profile = $projectDB->updateDocument($profile->getArrayCopy());
@ -441,7 +442,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
throw new Exception('Missing ID from OAuth2 provider', 400);
}
$current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret);
$current = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret);
if ($current) {
$projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401);
@ -451,7 +452,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'limit' => 1,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'oauth2'.\ucfirst($provider).'='.$oauth2ID,
'sessions.provider='.$provider,
'sessions.providerUid='.$oauth2ID
],
]) : $user;
@ -506,10 +508,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$secret = Auth::tokenGenerator();
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]],
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_LOGIN,
'provider' => $provider,
'providerUid' => $oauth2ID,
'providerToken' => $accessToken,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -527,10 +531,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
$user
->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID)
->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken)
->setAttribute('status', Auth::USER_STATUS_ACTIVATED)
->setAttribute('tokens', $session, Document::SET_TYPE_APPEND)
->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)
;
Authorization::setRole('user:'.$user->getId());
@ -648,10 +650,10 @@ App::post('/v1/account/sessions/anonymous')
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge(
[
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['user:' . $user['$id']], 'write' => ['user:' . $user['$id']]],
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_LOGIN,
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -663,7 +665,7 @@ App::post('/v1/account/sessions/anonymous')
$detector->getDevice()
));
$user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
$user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
Authorization::setRole('user:'.$user->getId());
@ -715,16 +717,18 @@ App::post('/v1/account/jwt')
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $user */
$tokens = $user->getAttribute('tokens', []);
$session = new Document();
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$session = $token;
foreach ($sessions as $session) {
/** @var Appwrite\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$current = $session;
}
}
if($session->isEmpty()) {
if($current->isEmpty()) {
throw new Exception('No valid session found', 401);
}
@ -738,7 +742,7 @@ App::post('/v1/account/jwt')
// 'scopes' => ['user'],
// 'iss' => 'http://api.mysite.com',
'userId' => $user->getId(),
'sessionId' => $session->getId(),
'sessionId' => $current->getId(),
])]), Response::MODEL_JWT);
});
@ -803,22 +807,19 @@ App::get('/v1/account/sessions')
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
$tokens = $user->getAttribute('tokens', []);
$sessions = [];
$sessions = $user->getAttribute('sessions', []);
$countries = $locale->getText('countries');
$current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret);
$current = Auth::sessionVerify($sessions, Auth::$secret);
foreach ($tokens as $token) { /* @var $token Document */
if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) {
continue;
}
foreach ($sessions as $key => $session) {
/** @var Document $session */
$token->setAttribute('countryName', (isset($countries[strtoupper($token->getAttribute('countryCode'))]))
? $countries[strtoupper($token->getAttribute('countryCode'))]
$session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))]))
? $countries[strtoupper($session->getAttribute('countryCode'))]
: $locale->getText('locale.country.unknown'));
$token->setAttribute('current', ($current == $token->getId()) ? true : false);
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
$sessions[] = $token;
$sessions[$key] = $session;
}
$response->dynamic(new Document([
@ -1146,7 +1147,7 @@ App::delete('/v1/account')
;
$events
->setParam('payload', $response->output($user, Response::MODEL_USER))
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
if (!Config::getParam('domainVerification')) {
@ -1191,14 +1192,16 @@ App::delete('/v1/account/sessions/:sessionId')
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$tokens = $user->getAttribute('tokens', []);
$sessions = $user->getAttribute('sessions', []);
foreach ($tokens as $token) { /* @var $token Document */
if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) {
if (!$projectDB->deleteDocument($token->getId())) {
foreach ($sessions as $session) {
/** @var Document $session */
if (($sessionId == $session->getId())) {
if (!$projectDB->deleteDocument($session->getId())) {
throw new Exception('Failed to remove token from DB', 500);
}
@ -1214,10 +1217,10 @@ App::delete('/v1/account/sessions/:sessionId')
;
}
$token->setAttribute('current', false);
$session->setAttribute('current', false);
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$token->setAttribute('current', true);
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$session->setAttribute('current', true);
$response
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
@ -1226,7 +1229,7 @@ App::delete('/v1/account/sessions/:sessionId')
}
$events
->setParam('payload', $response->output($token, Response::MODEL_SESSION))
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
;
return $response->noContent();
@ -1263,10 +1266,12 @@ App::delete('/v1/account/sessions')
/** @var Appwrite\Event\Event $events */
$protocol = $request->getProtocol();
$tokens = $user->getAttribute('tokens', []);
$sessions = $user->getAttribute('sessions', []);
foreach ($tokens as $token) { /* @var $token Document */
if (!$projectDB->deleteDocument($token->getId())) {
foreach ($sessions as $session) {
/** @var Document $session */
if (!$projectDB->deleteDocument($session->getId())) {
throw new Exception('Failed to remove token from DB', 500);
}
@ -1282,10 +1287,10 @@ App::delete('/v1/account/sessions')
;
}
$token->setAttribute('current', false);
$session->setAttribute('current', false);
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$token->setAttribute('current', true);
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$session->setAttribute('current', true);
$response
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
@ -1294,9 +1299,9 @@ App::delete('/v1/account/sessions')
}
$events
->setParam('payload', $response->output(new Document([
'sum' => count($tokens),
'sessions' => $tokens
->setParam('eventData', $response->output(new Document([
'sum' => count($sessions),
'sessions' => $sessions
]), Response::MODEL_SESSION_LIST))
;
@ -1419,7 +1424,7 @@ App::post('/v1/account/recovery')
;
$events
->setParam('payload',
->setParam('eventData',
$response->output($recovery->setAttribute('secret', $secret),
Response::MODEL_TOKEN
))
@ -1622,7 +1627,7 @@ App::post('/v1/account/verification')
;
$events
->setParam('payload',
->setParam('eventData',
$response->output($verification->setAttribute('secret', $verificationSecret),
Response::MODEL_TOKEN
))

View file

@ -14,7 +14,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Appwrite\Network\Validator\URL;
use Utopia\Validator\WhiteList;
$avatarCallback = function ($type, $code, $width, $height, $quality, $response) {

View file

@ -271,7 +271,7 @@ App::delete('/v1/database/collections/:collectionId')
;
$events
->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION))
->setParam('eventData', $response->output($collection, Response::MODEL_COLLECTION))
;
$audits
@ -294,7 +294,7 @@ App::post('/v1/database/collections/:collectionId/documents')
->label('sdk.description', '/docs/references/database/create-document.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ANY)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
->param('data', [], new JSON(), 'Document data as JSON object.')
->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
@ -401,7 +401,7 @@ App::post('/v1/database/collections/:collectionId/documents')
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($data, Response::MODEL_ANY)
->dynamic($data, Response::MODEL_DOCUMENT)
;
});
@ -478,7 +478,7 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
->label('sdk.description', '/docs/references/database/get-document.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ANY)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
->param('documentId', null, new UID(), 'Document unique ID.')
->inject('response')
@ -494,7 +494,7 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('No document found', 404);
}
$response->dynamic($document, Response::MODEL_ANY);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
App::patch('/v1/database/collections/:collectionId/documents/:documentId')
@ -508,7 +508,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
->label('sdk.description', '/docs/references/database/update-document.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ANY)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
->param('documentId', null, new UID(), 'Document unique ID.')
->param('data', [], new JSON(), 'Document data as JSON object.')
@ -566,7 +566,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
->setParam('data', $data->getArrayCopy())
;
$response->dynamic($data, Response::MODEL_ANY);
$response->dynamic($data, Response::MODEL_DOCUMENT);
});
App::delete('/v1/database/collections/:collectionId/documents/:documentId')
@ -614,9 +614,9 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
}
$events
->setParam('payload', $response->output($document, Response::MODEL_ANY))
->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT))
;
$audits
->setParam('event', 'database.documents.delete')
->setParam('resource', 'database/document/'.$document->getId())

View file

@ -6,7 +6,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\Validator\URL;
use Appwrite\Network\Validator\URL;
use Utopia\Validator\Range;
use Utopia\Config\Config;
use Utopia\Domains\Domain;

View file

@ -115,7 +115,7 @@ App::post('/v1/storage/files')
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag);
if (!$device->write($path, $data)) {
if (!$device->write($path, $data, $mimeType)) {
throw new Exception('Failed to save file', 500);
}
@ -242,13 +242,18 @@ App::get('/v1/storage/files/:fileId/preview')
->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true)
->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true)
->param('quality', 100, new Range(0, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
->param('borderWidth', 0, new Range(0, 100), 'Preview image border in pixels. Pass an integer between 0 to 100. Defaults to 0.', true)
->param('borderColor', '', new HexColor(), 'Preview image border color. Use a valid HEX color, no # is needed for prefix.', true)
->param('borderRadius', 0, new Range(0, 4000), 'Preview image border radius in pixels. Pass an integer between 0 to 4000.', true)
->param('opacity', 1, new Range(0,1), 'Preview image opacity. Only works with images having an alpha channel (like png). Pass a number between 0 to 1.', true)
->param('rotation', 0, new Range(0,360), 'Preview image rotation in degrees. Pass an integer between 0 and 360.', true)
->param('background', '', new HexColor(), 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true)
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
->inject('request')
->inject('response')
->inject('project')
->inject('projectDB')
->action(function ($fileId, $width, $height, $quality, $background, $output, $request, $response, $project, $projectDB) {
->action(function ($fileId, $width, $height, $quality, $borderWidth, $borderColor, $borderRadius, $opacity, $rotation, $background, $output, $request, $response, $project, $projectDB) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $project */
@ -273,7 +278,7 @@ App::get('/v1/storage/files/:fileId/preview')
$fileLogos = Config::getParam('storage-logos');
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache
$key = \md5($fileId.$width.$height.$quality.$background.$storage.$output);
$key = \md5($fileId.$width.$height.$quality.$borderWidth.$borderColor.$borderRadius.$opacity.$rotation.$background.$storage.$output);
$file = $projectDB->getDocument($fileId);
@ -293,7 +298,7 @@ App::get('/v1/storage/files/:fileId/preview')
$cipher = null;
$background = (empty($background)) ? 'eceff1' : $background;
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$key = \md5($path.$width.$height.$quality.$background.$storage.$output);
$key = \md5($path.$width.$height.$quality.$borderWidth.$borderColor.$borderRadius.$opacity.$rotation.$background.$storage.$output);
}
$compressor = new GZIP();
@ -337,11 +342,28 @@ App::get('/v1/storage/files/:fileId/preview')
$image = new Image($source);
$image->crop((int) $width, (int) $height);
if (!empty($opacity) || $opacity==0) {
$image->setOpacity($opacity);
}
if (!empty($background)) {
$image->setBackground('#'.$background);
}
if (!empty($borderWidth) ) {
$image->setBorder($borderWidth, '#'.$borderColor);
}
if (!empty($borderRadius)) {
$image->setBorderRadius($borderRadius);
}
if (!empty($rotation)) {
$image->setRotation($rotation);
}
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
@ -581,7 +603,7 @@ App::delete('/v1/storage/files/:fileId')
;
$events
->setParam('payload', $response->output($file, Response::MODEL_FILE))
->setParam('eventData', $response->output($file, Response::MODEL_FILE))
;
$response->noContent();

View file

@ -3,9 +3,9 @@
use Utopia\App;
use Utopia\Exception;
use Utopia\Config\Config;
use Utopia\Validator\Email;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Text;
use Utopia\Validator\Host;
use Appwrite\Network\Validator\Host;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
@ -243,7 +243,7 @@ App::delete('/v1/teams/:teamId')
}
$events
->setParam('payload', $response->output($team, Response::MODEL_TEAM))
->setParam('eventData', $response->output($team, Response::MODEL_TEAM))
;
$response->noContent();
@ -261,6 +261,7 @@ App::post('/v1/teams/:teamId/memberships')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team unique ID.')
->param('email', '', new Email(), 'New team member email.')
->param('name', '', new Text(128), 'New team member name. Max length: 128 chars.', true)
@ -327,6 +328,7 @@ App::post('/v1/teams/:teamId/memberships')
'registration' => \time(),
'reset' => false,
'name' => $name,
'sessions' => [],
'tokens' => [],
], ['email' => $email]);
} catch (Duplicate $th) {
@ -595,10 +597,11 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]],
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_LOGIN,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -606,7 +609,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
$user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
Authorization::setRole('user:'.$userId);
@ -711,7 +714,7 @@ App::delete('/v1/teams/:teamId/memberships/:inviteId')
;
$events
->setParam('payload', $response->output($membership, Response::MODEL_MEMBERSHIP))
->setParam('eventData', $response->output($membership, Response::MODEL_MEMBERSHIP))
;
$response->noContent();

View file

@ -4,7 +4,7 @@ use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Email;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Audit\Audit;
@ -196,21 +196,18 @@ App::get('/v1/users/:userId/sessions')
throw new Exception('User not found', 404);
}
$tokens = $user->getAttribute('tokens', []);
$sessions = [];
$sessions = $user->getAttribute('sessions', []);
$countries = $locale->getText('countries');
foreach ($tokens as $token) { /* @var $token Document */
if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) {
continue;
}
foreach ($sessions as $key => $session) {
/** @var Document $session */
$token->setAttribute('countryName', (isset($countries[strtoupper($token->getAttribute('countryCode'))]))
? $countries[strtoupper($token->getAttribute('countryCode'))]
$session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))]))
? $countries[strtoupper($session->getAttribute('countryCode'))]
: $locale->getText('locale.country.unknown'));
$token->setAttribute('current', false);
$session->setAttribute('current', false);
$sessions[] = $token;
$sessions[$key] = $session;
}
$response->dynamic(new Document([
@ -434,16 +431,18 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
throw new Exception('User not found', 404);
}
$tokens = $user->getAttribute('tokens', []);
$sessions = $user->getAttribute('sessions', []);
foreach ($tokens as $token) { /* @var $token Document */
if ($sessionId == $token->getId()) {
if (!$projectDB->deleteDocument($token->getId())) {
foreach ($sessions as $session) {
/** @var Document $session */
if ($sessionId == $session->getId()) {
if (!$projectDB->deleteDocument($session->getId())) {
throw new Exception('Failed to remove token from DB', 500);
}
$events
->setParam('payload', $response->output($user, Response::MODEL_USER))
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
}
}
@ -478,16 +477,18 @@ App::delete('/v1/users/:userId/sessions')
throw new Exception('User not found', 404);
}
$tokens = $user->getAttribute('tokens', []);
$sessions = $user->getAttribute('sessions', []);
foreach ($tokens as $token) { /* @var $token Document */
if (!$projectDB->deleteDocument($token->getId())) {
foreach ($sessions as $session) {
/** @var Document $session */
if (!$projectDB->deleteDocument($session->getId())) {
throw new Exception('Failed to remove token from DB', 500);
}
}
$events
->setParam('payload', $response->output($user, Response::MODEL_USER))
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
// TODO : Response filter implementation
@ -501,8 +502,8 @@ App::delete('/v1/users/:userId')
->label('scope', 'users.write')
->label('sdk.security', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteUser')
->label('sdk.description', '/docs/references/users/delete-user.md')
->label('sdk.method', 'delete')
->label('sdk.description', '/docs/references/users/delete.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('userId', '', function () {return new UID();}, 'User unique ID.')
@ -547,7 +548,7 @@ App::delete('/v1/users/:userId')
;
$events
->setParam('payload', $response->output($user, Response::MODEL_USER))
->setParam('eventData', $response->output($user, Response::MODEL_USER))
;
// TODO : Response filter implementation

View file

@ -78,7 +78,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
->setParam('projectId', $project->getId())
->setParam('userId', $user->getId())
->setParam('event', $route->getLabel('event', ''))
->setParam('payload', [])
->setParam('eventData', [])
->setParam('functionId', null)
->setParam('executionId', null)
->setParam('trigger', 'event')
@ -123,8 +123,8 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
/** @var bool $mode */
if (!empty($events->getParam('event'))) {
if(empty($events->getParam('payload'))) {
$events->setParam('payload', $response->getPayload());
if(empty($events->getParam('eventData'))) {
$events->setParam('eventData', $response->getPayload());
}
$webhooks = clone $events;

View file

@ -318,7 +318,7 @@ App::setResource('layout', function($locale) {
}, ['locale']);
App::setResource('locale', function() {
return new Locale('en');
return new Locale(App::getEnv('_APP_LOCALE', 'en'));
});
// Queues
@ -424,7 +424,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response
if (empty($user->getId()) // Check a document has been found in the DB
|| Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document
|| !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)) { // Validate user has valid login token
$user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]);
}

View file

@ -36,7 +36,7 @@ $cli
$projects = [$console];
$count = 0;
$migration = new Version\V06($register->get('db')); //TODO: remove hardcoded version and move to dynamic migration
$migration = new Version\V07($register->get('db')); //TODO: remove hardcoded version and move to dynamic migration
while ($sum > 0) {
foreach ($projects as $project) {

View file

@ -36,9 +36,6 @@ $maxCells = 10;
<div class="zone xl">
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/database/collection?id={{router.params.id}}&project={{router.params.project}}">
<a data-ls-if="{{project-collection.rules.length}} > 0" data-ls-attrs="href=/console/database/document?collection={{router.params.id}}&project={{router.params.project}}&buster={{project-collection.dateUpdated}}" class="button fly round text-align-center">
<i class="icon-plus"></i>
</a>
<h2>Documents</h2>
@ -135,7 +132,7 @@ $maxCells = 10;
</div>
</div>
<div class="clear text-align-center paging">
<div class="pull-end text-align-center paging">
<form
data-service="database.listDocuments"
data-event="submit"
@ -166,6 +163,10 @@ $maxCells = 10;
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-documents.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<a data-ls-if="{{project-collection.rules.length}} > 0" data-ls-attrs="href=/console/database/document?collection={{router.params.id}}&project={{router.params.project}}&buster={{project-collection.dateUpdated}}" class="button">
Add Document
</a>
</div>
</li>
<li data-state="/console/database/collection/settings?id={{router.params.id}}&project={{router.params.project}}">
@ -520,7 +521,7 @@ $maxCells = 10;
<div data-ls-loop="project-collections.collections" data-ls-as="project" data-ls-key="$index2" class="tiles cell-3 margin-bottom-negative">
<div class="margin-bottom" data-ls-if="{{project.$id}} != {{router.params.id}}">
<input type="radio" name="list" data-ls-attrs="value={{project.$id}},id={{project.$id}}" data-ls-bind="{{rule.list|firstElement}}" data-cast-to="array" required />
<input type="radio" data-ls-attrs="value={{project.$id}},id=[{{rule.$id}}].{{project.$id}},name=[{{rule.$id}}].list" data-ls-bind="{{rule.list|firstElement}}" data-cast-to="array" required />
<label data-ls-attrs="for={{project.$id}}"data-ls-bind="{{project.name}}"></label>
</div>
</div>

View file

@ -12,40 +12,6 @@
<li data-state="/console/database?project={{router.params.project}}">
<h2>Collections</h2>
<div data-ui-modal class="box modal close" data-button-text="" data-button-class="fly round" data-button-icon="icon-plus">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>New Collection</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Database Collection"
data-service="database.createCollection"
data-event="submit"
data-scope="sdk"
data-success="alert,reset,redirect,trigger"
data-success-param-alert-text="Collection created successfully"
data-success-param-redirect-url="/console/database/collection/settings?id={{serviceData.$id}}&project={{router.params.project}}"
data-success-param-trigger-events="database.createCollection"
data-failure="alert"
data-failure-param-alert-text="Failed to create collection"
data-failure-param-alert-classname="error">
<label for="user-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
<div class="margin-top"
data-service="database.listCollections"
data-event="load,database.createCollection,database.updateCollection,database.deleteCollection"
@ -76,7 +42,7 @@
</ul>
</div>
<div class="clear text-align-center paging">
<div class="pull-end text-align-center paging">
<form
data-service="database.listCollections"
data-event="submit"
@ -105,6 +71,41 @@
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-collections.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-text="Add Collection">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>New Collection</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Database Collection"
data-service="database.createCollection"
data-event="submit"
data-scope="sdk"
data-success="alert,reset,redirect,trigger"
data-success-param-alert-text="Collection created successfully"
data-success-param-redirect-url="/console/database/collection/settings?id={{serviceData.$id}}&project={{router.params.project}}"
data-success-param-trigger-events="database.createCollection"
data-failure="alert"
data-failure-param-alert-text="Failed to create collection"
data-failure-param-alert-classname="error">
<label for="user-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</li>
<!-- <li data-state="/console/database/usage?project={{router.params.project}}">
<h2>Usage</h2>

View file

@ -17,48 +17,6 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<li data-state="/console/database?project={{router.params.project}}">
<h2 class="margin-bottom">Files</h2>
<div data-ui-modal class="box modal sticky-footer close" data-button-text="" data-button-class="fly round" data-button-icon="icon-plus">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Upload File</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Storage File"
data-service="storage.createFile"
data-event="submit"
data-scope="sdk"
data-loading="Uploading File..."
data-success="alert,trigger,reset"
data-success-param-alert-text="File uploaded successfully"
data-success-param-trigger-events="storage.createFile"
data-failure="alert"
data-failure-param-alert-text="Failed to upload file"
data-failure-param-alert-classname="error">
<input type="hidden" name="folderId" id="files-folderId" data-cast-to="integer" value="1">
<label for="file-read">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(['*'])); ?>" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add * for wildcard access</div>
<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 * for wildcard access</div>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
<form class="box padding-small margin-bottom search"
data-service="storage.listFiles"
data-event="submit"
@ -215,7 +173,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
</div>
<div class="clear text-align-center paging">
<div class="pull-end text-align-center paging">
<form
data-service="storage.listFiles"
data-event="submit"
@ -244,6 +202,48 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-files.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="box modal sticky-footer close" data-button-text="Add File">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Upload File</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Storage File"
data-service="storage.createFile"
data-event="submit"
data-scope="sdk"
data-loading="Uploading File..."
data-success="alert,trigger,reset"
data-success-param-alert-text="File uploaded successfully"
data-success-param-trigger-events="storage.createFile"
data-failure="alert"
data-failure-param-alert-text="Failed to upload file"
data-failure-param-alert-classname="error">
<input type="hidden" name="folderId" id="files-folderId" data-cast-to="integer" value="1">
<label for="file-read">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(['*'])); ?>" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add * for wildcard access</div>
<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 * for wildcard access</div>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</footer>
</form>
</div>
</div>
</li>
</ul>

View file

@ -17,41 +17,6 @@ $providers = $this->getParam('providers', []);
<h2>Users</h2>
<div data-ui-modal class="box modal close" data-button-text="" data-button-class="fly round" data-button-icon="icon-plus">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Create User</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create User"
data-service="users.create"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created user successfully"
data-success-param-trigger-events="users.create"
data-failure="alert"
data-failure-param-alert-text="Failed to create user"
data-failure-param-alert-classname="error">
<label for="user-name">Name</label>
<input type="text" class="full-width" id="user-name" name="name" required autocomplete="off" maxlength="128" />
<label for="user-email">Email</label>
<input type="email" class="full-width" id="user-email" name="email" required autocomplete="off" />
<label for="user-password">Password</label>
<input type="password" class="full-width" id="user-password" name="password" required pattern=".{6,}" title="Six or more characters" autocomplete="off" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
<form class="box padding-small margin-bottom search"
data-service="users.list"
data-event="submit"
@ -137,7 +102,7 @@ $providers = $this->getParam('providers', []);
</div>
</div>
<div class="clear text-align-center paging">
<div class="pull-end text-align-center paging">
<form
data-service="users.list"
data-event="submit"
@ -166,41 +131,47 @@ $providers = $this->getParam('providers', []);
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-users.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="box modal close" data-button-text="Add User">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Create User</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create User"
data-service="users.create"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created user successfully"
data-success-param-trigger-events="users.create"
data-failure="alert"
data-failure-param-alert-text="Failed to create user"
data-failure-param-alert-classname="error">
<label for="user-name">Name</label>
<input type="text" class="full-width" id="user-name" name="name" required autocomplete="off" maxlength="128" />
<label for="user-email">Email</label>
<input type="email" class="full-width" id="user-email" name="email" required autocomplete="off" />
<label for="user-password">Password</label>
<input type="password" class="full-width" id="user-password" name="password" required pattern=".{6,}" title="Six or more characters" autocomplete="off" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</div>
</li>
<li data-state="/console/users/teams?project={{router.params.project}}">
<h2>Teams</h2>
<div data-ui-modal class="box modal close" data-button-text="" data-button-class="fly round" data-button-icon="icon-plus">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Create Team</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Team"
data-service="teams.create"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created team successfully"
data-success-param-trigger-events="filter-teams-changed,teams.create"
data-failure="alert"
data-failure-param-alert-text="Failed to create team"
data-failure-param-alert-classname="error">
<label for="team-name">Name</label>
<input type="text" class="full-width" id="team-name" name="name" required autocomplete="off" maxlength="128" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
<form class="box padding-small margin-bottom search"
data-service="teams.list"
data-event="submit"
@ -239,7 +210,7 @@ $providers = $this->getParam('providers', []);
</div>
<div data-ls-if="0 != {{project-teams.sum}}">
<div class="margin-bottom-small margin-end-small text-align-end text-size-small"><span data-ls-bind="{{project-teams.sum}}"></span> results found</div>
<div class="margin-bottom-small margin-end-small text-align-end text-size-small"><span data-ls-bind="{{project-teams.sum}}"></span> results found</div>
<div class="box margin-bottom">
<table class="vertical">
@ -267,7 +238,7 @@ $providers = $this->getParam('providers', []);
</div>
</div>
<div class="clear text-align-center paging">
<div class="pull-end text-align-center paging">
<form
data-service="teams.list"
data-event="submit"
@ -296,6 +267,35 @@ $providers = $this->getParam('providers', []);
<button name="offset" data-paging-next data-offset="{{router.params.offset}}" data-sum="{{project-teams.sum}}" class="margin-start round small" aria-label="Next"><i class="icon-right-open"></i></button>
</form>
</div>
<div data-ui-modal class="box modal close" data-button-text="Add Team">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>Create Team</h1>
<form
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Team"
data-service="teams.create"
data-event="submit"
data-success="alert,trigger,reset"
data-success-param-alert-text="Created team successfully"
data-success-param-trigger-events="filter-teams-changed,teams.create"
data-failure="alert"
data-failure-param-alert-text="Failed to create team"
data-failure-param-alert-classname="error">
<label for="team-name">Name</label>
<input type="text" class="full-width" id="team-name" name="name" required autocomplete="off" maxlength="128" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
</form>
</div>
</div>
</li>

View file

@ -52,10 +52,11 @@ services:
depends_on:
- mariadb
- redis
- clamav
# - clamav
- influxdb
environment:
- _APP_ENV
- _APP_LOCALE
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_IPS
- _APP_SYSTEM_EMAIL_NAME
@ -351,14 +352,14 @@ services:
volumes:
- appwrite-redis:/data:rw
clamav:
image: appwrite/clamav:1.2.0
container_name: appwrite-clamav
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads
# clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# restart: unless-stopped
# networks:
# - appwrite
# volumes:
# - appwrite-uploads:/storage/uploads
influxdb:
image: influxdb:1.8-alpine

View file

@ -112,13 +112,21 @@ class DeletesV1
protected function deleteUser(Document $document, $projectId)
{
$tokens = $document->getAttribute('tokens', []);
foreach ($tokens as $token) {
if (!$this->getProjectDB($projectId)->deleteDocument($token->getId())) {
throw new Exception('Failed to remove token from DB');
}
}
$sessions = $document->getAttribute('sessions', []);
foreach ($sessions as $session) {
if (!$this->getProjectDB($projectId)->deleteDocument($session->getId())) {
throw new Exception('Failed to remove session from DB');
}
}
// Delete Memberships
$this->deleteByGroup([
'$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS,

View file

@ -147,7 +147,7 @@ class FunctionsV1
$trigger = $this->args['trigger'] ?? '';
$event = $this->args['event'] ?? '';
$scheduleOriginal = $this->args['scheduleOriginal'] ?? '';
$payload = (!empty($this->args['payload'])) ? json_encode($this->args['payload']) : '';
$eventData = (!empty($this->args['eventData'])) ? json_encode($this->args['eventData']) : '';
$data = $this->args['data'] ?? '';
$userId = $this->args['userId'] ?? '';
$jwt = $this->args['jwt'] ?? '';
@ -198,7 +198,7 @@ class FunctionsV1
Console::success('Triggered function: '.$event);
$this->execute('event', $projectId, '', $database, $function, $event, $payload, $data, $userId, $jwt);
$this->execute('event', $projectId, '', $database, $function, $event, $eventData, $data, $userId, $jwt);
}
}
break;
@ -254,7 +254,7 @@ class FunctionsV1
'scheduleOriginal' => $function->getAttribute('schedule', ''),
]); // Async task rescheduale
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt);
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$eventData*/'', $data, $userId, $jwt);
break;
case 'http':
@ -266,7 +266,7 @@ class FunctionsV1
throw new Exception('Function not found ('.$functionId.')');
}
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt);
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$eventData*/'', $data, $userId, $jwt);
break;
default:
@ -284,12 +284,12 @@ class FunctionsV1
* @param Database $database
* @param Database $function
* @param string $event
* @param string $payload
* @param string $eventData
* @param string $data
*
* @return void
*/
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $data = '', string $userId = '', string $jwt = ''): void
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $eventData = '', string $data = '', string $userId = '', string $jwt = ''): void
{
global $list;
@ -343,7 +343,7 @@ class FunctionsV1
'APPWRITE_FUNCTION_ENV_NAME' => $environment['name'],
'APPWRITE_FUNCTION_ENV_VERSION' => $environment['version'],
'APPWRITE_FUNCTION_EVENT' => $event,
'APPWRITE_FUNCTION_EVENT_PAYLOAD' => $payload,
'APPWRITE_FUNCTION_EVENT_DATA' => $eventData,
'APPWRITE_FUNCTION_DATA' => $data,
'APPWRITE_FUNCTION_USER_ID' => $userId,
'APPWRITE_FUNCTION_JWT' => $jwt,
@ -482,7 +482,7 @@ class FunctionsV1
->setParam('projectId', $projectId)
->setParam('userId', $userId)
->setParam('event', 'functions.executions.update')
->setParam('payload', [
->setParam('eventData', [
'$id' => $execution['$id'],
'functionId' => $execution['functionId'],
'dateCreated' => $execution['dateCreated'],

View file

@ -37,7 +37,7 @@ class WebhooksV1
$projectId = $this->args['projectId'] ?? '';
$userId = $this->args['userId'] ?? '';
$event = $this->args['event'] ?? '';
$payload = \json_encode($this->args['payload']);
$eventData = \json_encode($this->args['eventData']);
// Webhook
@ -67,7 +67,7 @@ class WebhooksV1
$ch = \curl_init($url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
\curl_setopt($ch, CURLOPT_POSTFIELDS, $eventData);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(APP_USERAGENT,
@ -79,7 +79,7 @@ class WebhooksV1
CURLOPT_HTTPHEADER,
[
'Content-Type: application/json',
'Content-Length: '.\strlen($payload),
'Content-Length: '.\strlen($eventData),
'X-'.APP_NAME.'-Webhook-Id: '.$id,
'X-'.APP_NAME.'-Webhook-Event: '.$event,
'X-'.APP_NAME.'-Webhook-Name: '.$name,

View file

@ -51,7 +51,7 @@
"utopia-php/swoole": "0.2.*",
"utopia-php/system": "0.4.*",
"utopia-php/storage": "0.4.*",
"utopia-php/image": "0.1.*",
"utopia-php/image": "0.2.*",
"resque/php-resque": "1.3.6",
"matomo/device-detector": "4.1.0",

166
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": "2603ff45d619697717c90bf09603ff21",
"content-hash": "60b57e034676287a703cf42b1de0c60d",
"packages": [
{
"name": "adhocore/jwt",
@ -1755,16 +1755,16 @@
},
{
"name": "utopia-php/image",
"version": "0.1.0",
"version": "0.2.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "66e38db211b1d6fe93de09d82606641e0f996e42"
"reference": "0754955a165483852184d1215cc3bf659432d23a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/66e38db211b1d6fe93de09d82606641e0f996e42",
"reference": "66e38db211b1d6fe93de09d82606641e0f996e42",
"url": "https://api.github.com/repos/utopia-php/image/zipball/0754955a165483852184d1215cc3bf659432d23a",
"reference": "0754955a165483852184d1215cc3bf659432d23a",
"shasum": ""
},
"require": {
@ -1802,9 +1802,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.1.0"
"source": "https://github.com/utopia-php/image/tree/0.2.1"
},
"time": "2021-02-19T05:09:46+00:00"
"time": "2021-04-13T07:47:24+00:00"
},
{
"name": "utopia-php/locale",
@ -2276,12 +2276,12 @@
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
"reference": "f813a658f0446192c5e17f96727070ee9342b93a"
"reference": "7a64a9ad336fc5e1e70b1c1fc1e9618a7027332e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/byte-stream/zipball/f813a658f0446192c5e17f96727070ee9342b93a",
"reference": "f813a658f0446192c5e17f96727070ee9342b93a",
"url": "https://api.github.com/repos/amphp/byte-stream/zipball/7a64a9ad336fc5e1e70b1c1fc1e9618a7027332e",
"reference": "7a64a9ad336fc5e1e70b1c1fc1e9618a7027332e",
"shasum": ""
},
"require": {
@ -2346,7 +2346,7 @@
"type": "github"
}
],
"time": "2020-08-30T19:23:04+00:00"
"time": "2021-04-05T20:23:22+00:00"
},
{
"name": "appwrite/sdk-generator",
@ -3587,12 +3587,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "b2ce4cf415b9989fac88e8c27c39b5ba2faad72b"
"reference": "97eb187efc3560da69c5b501235cd3eb1ebfec86"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/b2ce4cf415b9989fac88e8c27c39b5ba2faad72b",
"reference": "b2ce4cf415b9989fac88e8c27c39b5ba2faad72b",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/97eb187efc3560da69c5b501235cd3eb1ebfec86",
"reference": "97eb187efc3560da69c5b501235cd3eb1ebfec86",
"shasum": ""
},
"require": {
@ -3640,7 +3640,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:19+00:00"
"time": "2021-04-02T08:22:03+00:00"
},
{
"name": "phpunit/php-invoker",
@ -3648,12 +3648,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-invoker.git",
"reference": "e2905d5648ac5e9bd0aa85b50d240e5890f76493"
"reference": "de89b92643f75d97135fd0f895d4369630952c95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/e2905d5648ac5e9bd0aa85b50d240e5890f76493",
"reference": "e2905d5648ac5e9bd0aa85b50d240e5890f76493",
"url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/de89b92643f75d97135fd0f895d4369630952c95",
"reference": "de89b92643f75d97135fd0f895d4369630952c95",
"shasum": ""
},
"require": {
@ -3704,7 +3704,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:27+00:00"
"time": "2021-04-02T08:22:12+00:00"
},
{
"name": "phpunit/php-text-template",
@ -3712,12 +3712,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "e6a2483ffd3659d723996fb8b2ca638244b87e7c"
"reference": "f46a87d94ad351b46c836f6cdda98795e8a6c979"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e6a2483ffd3659d723996fb8b2ca638244b87e7c",
"reference": "e6a2483ffd3659d723996fb8b2ca638244b87e7c",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/f46a87d94ad351b46c836f6cdda98795e8a6c979",
"reference": "f46a87d94ad351b46c836f6cdda98795e8a6c979",
"shasum": ""
},
"require": {
@ -3764,7 +3764,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:59+00:00"
"time": "2021-04-02T08:22:46+00:00"
},
{
"name": "phpunit/php-timer",
@ -3772,12 +3772,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "c0187813193d3709a455b94916bbee2881a1c6e3"
"reference": "bd80d581ad411a5a4b7e613541a7f4cd09cf0da9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/c0187813193d3709a455b94916bbee2881a1c6e3",
"reference": "c0187813193d3709a455b94916bbee2881a1c6e3",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/bd80d581ad411a5a4b7e613541a7f4cd09cf0da9",
"reference": "bd80d581ad411a5a4b7e613541a7f4cd09cf0da9",
"shasum": ""
},
"require": {
@ -3824,7 +3824,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:35+00:00"
"time": "2021-04-02T08:22:20+00:00"
},
{
"name": "phpunit/phpunit",
@ -3983,12 +3983,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
"reference": "845853b8c553f6b61d9a708b8f26066806bcc7dd"
"reference": "e59dfbeeaf6ccdff168c537427cbc9f7fed6b160"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/845853b8c553f6b61d9a708b8f26066806bcc7dd",
"reference": "845853b8c553f6b61d9a708b8f26066806bcc7dd",
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/e59dfbeeaf6ccdff168c537427cbc9f7fed6b160",
"reference": "e59dfbeeaf6ccdff168c537427cbc9f7fed6b160",
"shasum": ""
},
"require": {
@ -4032,7 +4032,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:18:24+00:00"
"time": "2021-04-02T08:23:11+00:00"
},
{
"name": "sebastian/code-unit",
@ -4096,12 +4096,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
"reference": "ab4d610891809670894a4fc260c17e5d5960ba4c"
"reference": "f7ee1d817bdd8a8bdfb76b11fa851204132ef6d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ab4d610891809670894a4fc260c17e5d5960ba4c",
"reference": "ab4d610891809670894a4fc260c17e5d5960ba4c",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/f7ee1d817bdd8a8bdfb76b11fa851204132ef6d9",
"reference": "f7ee1d817bdd8a8bdfb76b11fa851204132ef6d9",
"shasum": ""
},
"require": {
@ -4144,7 +4144,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:16+00:00"
"time": "2021-04-02T08:20:56+00:00"
},
{
"name": "sebastian/comparator",
@ -4152,12 +4152,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "d3d66b8faa86ac57e1236d576ad003f73097c9cd"
"reference": "604de433dd3e1467ded9a92414b3561a812554c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d3d66b8faa86ac57e1236d576ad003f73097c9cd",
"reference": "d3d66b8faa86ac57e1236d576ad003f73097c9cd",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/604de433dd3e1467ded9a92414b3561a812554c5",
"reference": "604de433dd3e1467ded9a92414b3561a812554c5",
"shasum": ""
},
"require": {
@ -4219,7 +4219,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:24+00:00"
"time": "2021-04-02T08:21:05+00:00"
},
{
"name": "sebastian/complexity",
@ -4284,12 +4284,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "e81849c6dfbe34442b4685fa457fd6f012370e54"
"reference": "3fa178cf5772d7a09bcfe01bfb7acc13edcf8aec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e81849c6dfbe34442b4685fa457fd6f012370e54",
"reference": "e81849c6dfbe34442b4685fa457fd6f012370e54",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3fa178cf5772d7a09bcfe01bfb7acc13edcf8aec",
"reference": "3fa178cf5772d7a09bcfe01bfb7acc13edcf8aec",
"shasum": ""
},
"require": {
@ -4343,7 +4343,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:31+00:00"
"time": "2021-04-02T08:21:13+00:00"
},
{
"name": "sebastian/environment",
@ -4351,12 +4351,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "c25633688d84a9f8694989223479051b5a8a23e7"
"reference": "1fe23c75ca20a9cfe5ef8af473c726f9ef7e9465"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c25633688d84a9f8694989223479051b5a8a23e7",
"reference": "c25633688d84a9f8694989223479051b5a8a23e7",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1fe23c75ca20a9cfe5ef8af473c726f9ef7e9465",
"reference": "1fe23c75ca20a9cfe5ef8af473c726f9ef7e9465",
"shasum": ""
},
"require": {
@ -4407,7 +4407,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:40+00:00"
"time": "2021-04-02T08:21:21+00:00"
},
{
"name": "sebastian/exporter",
@ -4415,12 +4415,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "9119858d42f3963d01b737f029bb90f8464fd0ca"
"reference": "31abe95278f9b406051b4a26faf2677fbcc01755"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/9119858d42f3963d01b737f029bb90f8464fd0ca",
"reference": "9119858d42f3963d01b737f029bb90f8464fd0ca",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/31abe95278f9b406051b4a26faf2677fbcc01755",
"reference": "31abe95278f9b406051b4a26faf2677fbcc01755",
"shasum": ""
},
"require": {
@ -4485,7 +4485,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:48+00:00"
"time": "2021-04-02T08:21:30+00:00"
},
{
"name": "sebastian/global-state",
@ -4493,12 +4493,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "fe610de5530e3d29007134f76ee8dc79581a607d"
"reference": "8684de2d84cd6a819f5e22a1292cd3b2e1f26487"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/fe610de5530e3d29007134f76ee8dc79581a607d",
"reference": "fe610de5530e3d29007134f76ee8dc79581a607d",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/8684de2d84cd6a819f5e22a1292cd3b2e1f26487",
"reference": "8684de2d84cd6a819f5e22a1292cd3b2e1f26487",
"shasum": ""
},
"require": {
@ -4550,7 +4550,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:16:56+00:00"
"time": "2021-04-02T08:21:38+00:00"
},
{
"name": "sebastian/lines-of-code",
@ -4615,12 +4615,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
"reference": "01ab82e49081de59e2da70c351d5f698c77c33c5"
"reference": "b331fc5975a2225e6b93c7e6cd9fce0a57f0fddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/01ab82e49081de59e2da70c351d5f698c77c33c5",
"reference": "01ab82e49081de59e2da70c351d5f698c77c33c5",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b331fc5975a2225e6b93c7e6cd9fce0a57f0fddc",
"reference": "b331fc5975a2225e6b93c7e6cd9fce0a57f0fddc",
"shasum": ""
},
"require": {
@ -4665,7 +4665,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:03+00:00"
"time": "2021-04-02T08:21:47+00:00"
},
{
"name": "sebastian/object-reflector",
@ -4673,12 +4673,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
"reference": "7ed67aee59862b40785138f0203e86f1fde1b93a"
"reference": "297a334e3ae78670a7633e36569d7362bb7397bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/7ed67aee59862b40785138f0203e86f1fde1b93a",
"reference": "7ed67aee59862b40785138f0203e86f1fde1b93a",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/297a334e3ae78670a7633e36569d7362bb7397bf",
"reference": "297a334e3ae78670a7633e36569d7362bb7397bf",
"shasum": ""
},
"require": {
@ -4721,7 +4721,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:11+00:00"
"time": "2021-04-02T08:21:54+00:00"
},
{
"name": "sebastian/recursion-context",
@ -4729,12 +4729,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "249976376508ed7e83b6dc429cd883a44b2a3c51"
"reference": "78526ace5bac7c10048020f0317c58fd310a14ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/249976376508ed7e83b6dc429cd883a44b2a3c51",
"reference": "249976376508ed7e83b6dc429cd883a44b2a3c51",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/78526ace5bac7c10048020f0317c58fd310a14ec",
"reference": "78526ace5bac7c10048020f0317c58fd310a14ec",
"shasum": ""
},
"require": {
@ -4785,7 +4785,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:43+00:00"
"time": "2021-04-02T08:22:30+00:00"
},
{
"name": "sebastian/resource-operations",
@ -4849,12 +4849,12 @@
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "e02c851008e26557b4f1b4ffd139b71c96937b04"
"reference": "17fc98bb3c75a02a1a99ffdd022e84ac6d22bd51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e02c851008e26557b4f1b4ffd139b71c96937b04",
"reference": "e02c851008e26557b4f1b4ffd139b71c96937b04",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/17fc98bb3c75a02a1a99ffdd022e84ac6d22bd51",
"reference": "17fc98bb3c75a02a1a99ffdd022e84ac6d22bd51",
"shasum": ""
},
"require": {
@ -4898,7 +4898,7 @@
"type": "github"
}
],
"time": "2021-03-17T06:17:51+00:00"
"time": "2021-04-02T08:36:52+00:00"
},
{
"name": "sebastian/version",
@ -4997,12 +4997,12 @@
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "fb7e2447d39984358343685fb9f0e800cd79e6a3"
"reference": "9a90698d4624b85a578007a00312338d3adecaf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/fb7e2447d39984358343685fb9f0e800cd79e6a3",
"reference": "fb7e2447d39984358343685fb9f0e800cd79e6a3",
"url": "https://api.github.com/repos/symfony/console/zipball/9a90698d4624b85a578007a00312338d3adecaf8",
"reference": "9a90698d4624b85a578007a00312338d3adecaf8",
"shasum": ""
},
"require": {
@ -5088,7 +5088,7 @@
"type": "tidelift"
}
],
"time": "2021-03-28T09:44:11+00:00"
"time": "2021-04-09T09:54:19+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -5576,12 +5576,12 @@
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "1309413986521646bb0ba91140afdc2a61ed8cfe"
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/1309413986521646bb0ba91140afdc2a61ed8cfe",
"reference": "1309413986521646bb0ba91140afdc2a61ed8cfe",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb",
"shasum": ""
},
"require": {
@ -5648,7 +5648,7 @@
"type": "tidelift"
}
],
"time": "2021-03-23T23:28:01+00:00"
"time": "2021-04-01T10:43:52+00:00"
},
{
"name": "symfony/string",
@ -5790,12 +5790,12 @@
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "116bfb0bc9ec2a39db93431b7fe67144164d251e"
"reference": "f7250c6ea6b6cdd724e25ce7c56e2a60006203cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/116bfb0bc9ec2a39db93431b7fe67144164d251e",
"reference": "116bfb0bc9ec2a39db93431b7fe67144164d251e",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/f7250c6ea6b6cdd724e25ce7c56e2a60006203cb",
"reference": "f7250c6ea6b6cdd724e25ce7c56e2a60006203cb",
"shasum": ""
},
"require": {
@ -5861,7 +5861,7 @@
"type": "tidelift"
}
],
"time": "2021-03-22T08:23:49+00:00"
"time": "2021-04-10T08:17:25+00:00"
},
{
"name": "vimeo/psalm",

View file

@ -71,10 +71,11 @@ services:
depends_on:
- mariadb
- redis
- clamav
# - clamav
- influxdb
environment:
- _APP_ENV
- _APP_LOCALE
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
@ -413,13 +414,13 @@ services:
volumes:
- appwrite-redis:/data:rw
clamav:
image: appwrite/clamav:1.2.0
container_name: appwrite-clamav
networks:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads
# clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# networks:
# - appwrite
# volumes:
# - appwrite-uploads:/storage/uploads
influxdb:
image: influxdb:1.8-alpine

View file

@ -9,6 +9,9 @@ docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
echo 'Deno 1.6...'
docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le -t appwrite/env-deno-1.6:1.0.0 ./docker/environments/deno-1.6/ --push
echo 'Deno 1.8...'
docker buildx build --platform linux/amd64,linux/386 -t appwrite/env-deno-1.8:1.0.0 ./docker/environments/deno-1.8/ --push
echo 'Node 14.5...'
docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le -t appwrite/env-node-14.5:1.0.0 ./docker/environments/node-14.5/ --push
@ -24,6 +27,9 @@ docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
echo 'Python 3.8...'
docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le -t appwrite/env-python-3.8:1.0.0 ./docker/environments/python-3.8/ --push
echo 'Python 3.9...'
docker buildx build --platform linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le -t appwrite/env-python-3.9:1.0.0 ./docker/environments/python-3.9/ --push
echo 'Ruby 2.7...'
docker buildx build --platform linux/amd64,linux/arm64,linux/386,linux/ppc64le -t appwrite/env-ruby-2.7:1.0.2 ./docker/environments/ruby-2.7/ --push
@ -33,6 +39,9 @@ docker buildx build --platform linux/amd64,linux/arm64,linux/386,linux/ppc64le -
echo 'Dart 2.10...'
docker buildx build --platform linux/amd64 -t appwrite/env-dart-2.10:1.0.0 ./docker/environments/dart-2.10/ --push
echo 'Dart 2.12...'
docker buildx build --platform linux/amd64 -t appwrite/env-dart-2.12:1.0.0 ./docker/environments/dart-2.12/ --push
echo '.NET 3.1...'
docker buildx build --platform linux/amd64,linux/arm64 -t appwrite/env-dotnet-3.1:1.0.0 ./docker/environments/dotnet-3.1/ --push

View file

@ -0,0 +1,9 @@
FROM google/dart:2.12
LABEL maintainer="team@appwrite.io"
RUN apt-get update -y && apt-get install -y tar
WORKDIR /usr/local/src/
ENV PUB_CACHE=/usr/local/src/.appwrite

View file

@ -0,0 +1,11 @@
FROM hayd/deno:alpine-1.8.3
LABEL maintainer="team@appwrite.io"
RUN apk add tar
RUN mkdir /usr/local/src
WORKDIR /usr/local/src/
ENV DENO_DIR=/usr/local/src/.appwrite

View file

@ -0,0 +1,11 @@
FROM python:3.9-alpine
LABEL maintainer="team@appwrite.io"
RUN apk add tar
RUN mkdir /usr/local/src
WORKDIR /usr/local/src/
ENV PYTHONPATH "${PYTHONPATH}:/usr/local/src/.appwrite"

View file

@ -11,6 +11,7 @@ void main() async {
.setEndpoint('http://[HOSTNAME_OR_IP]/v1') // Make sure your endpoint is accessible
.setProject('5ff3379a01d25') // Your project ID
.setKey('cd868c7af8bdc893b4...93b7535db89')
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
Users users = Users(client);

View file

@ -10,6 +10,7 @@ client
.setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
.setProject('5df5acd0d48c2') // Your project ID
.setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
```
@ -41,6 +42,7 @@ client
.setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
.setProject('5df5acd0d48c2') // Your project ID
.setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
let promise = users.create('email@example.com', 'password');

View file

@ -14,6 +14,7 @@ static async Task Main(string[] args)
.setEndpoint('http://[HOSTNAME_OR_IP]/v1') // Make sure your endpoint is accessible
.setProject('5ff3379a01d25') // Your project ID
.setKey('cd868c7af8bdc893b4...93b7535db89')
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
var users = Users(client);

View file

@ -58,7 +58,7 @@ Client client = Client();
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Remove in production
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
```
@ -91,7 +91,7 @@ Client client = Client();
client
.setEndpoint('https://localhost/v1') // Your Appwrite Endpoint
.setProject('5e8cf4f46b5e8') // Your project ID
.setSelfSigned() // Remove in production
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;

View file

@ -12,6 +12,7 @@ client
.setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
.setProject('5df5acd0d48c2') // Your project ID
.setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
```
@ -40,6 +41,7 @@ client
.setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
.setProject('5df5acd0d48c2') // Your project ID
.setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
let users = new sdk.Users(client);

View file

@ -10,6 +10,7 @@ $client
->setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
->setProject('5df5acd0d48c2') // Your project ID
->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
->setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
```
@ -33,6 +34,7 @@ $client
->setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
->setProject('5df5acd0d48c2') // Your project ID
->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
->setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
$users = new Users($client);

View file

@ -13,6 +13,7 @@ client = Client()
.set_endpoint('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint
.set_project('5df5acd0d48c2') # Your project ID
.set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key
.set_self_signed() # Use only on dev mode with a self-signed SSL cert
)
```
@ -36,6 +37,7 @@ client = Client()
.set_endpoint('https://[HOSTNAME_OR_IP]/v1') # Your API Endpoint
.set_project('5df5acd0d48c2') # Your project ID
.set_key('919c2d18fb5d4...a2ae413da83346ad2') # Your secret API key
.set_self_signed() # Use only on dev mode with a self-signed SSL cert
)
users = Users(client)

View file

@ -12,6 +12,7 @@ client
.set_endpoint(ENV["APPWRITE_ENDPOINT"]) # Your API Endpoint
.set_project(ENV["APPWRITE_PROJECT"]) # Your project ID
.set_key(ENV["APPWRITE_SECRET"]) # Your secret API key
.setSelfSigned() # Use only on dev mode with a self-signed SSL cert
;
```
@ -34,6 +35,7 @@ client
.set_endpoint(ENV["APPWRITE_ENDPOINT"]) # Your API Endpoint
.set_project(ENV["APPWRITE_PROJECT"]) # Your project ID
.set_key(ENV["APPWRITE_SECRET"]) # Your secret API key
.setSelfSigned() # Use only on dev mode with a self-signed SSL cert
;
users = Appwrite::Users.new(client);

View file

@ -15,6 +15,7 @@ const appwrite = new Appwrite();
appwrite
.setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
.setProject('455x34dfkj') // Your project ID
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
```
@ -41,6 +42,7 @@ const appwrite = new Appwrite();
appwrite
.setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
.setProject('455x34dfkj')
.setSelfSigned() // Use only on dev mode with a self-signed SSL cert
;
// Register User

View file

@ -1,45 +0,0 @@
# Running in Production
This tutorial will cover some basic concepts and best practices for running a production Appwrite server. This tutorial assumes you have some basic knowledge of Docker and Docker Compose command-line tools.
## Error Reporting
By default, Appwrite installation comes with error debugging turned on, We do this to help new users solve issues and report problems while still in development mode.
In production, it is highly recommended to turn error reporting off. To do so, you have to change the Appwrite container environment variable **_APP_ENV** value from **development** to **production**.
## Enable Encryption
By default, the Appwrite setup doesnt come with a uniquely generated encryption key. This key is used to store your files and sensitive data like webhook passwords or API keys in a safe way. To take advantage of this feature, you must generate a unique key and set it as the value of the **_APP_OPENSSL_KEY_V1** environment variable.
Make sure to keep this key in a safe place and never make it publicly accessible. There are many [online resources]([https://www.freecodecamp.org/news/how-to-securely-store-api-keys-4ff3ea19ebda/](https://www.freecodecamp.org/news/how-to-securely-store-api-keys-4ff3ea19ebda/)) with methods of keeping your secret keys safe in your servers.
## Limit Access to your Console
By default, anyone can signup for your Appwrite server, create projects, and use your computing power. While this is great for testing around or running your Appwrite service in a network isolated environment, it is highly not recommended for public production use.
We are providing three different methods to limit access to your Appwrite console. You can either set a list of [IPs]([https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_ips](https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_ips)), [email address]([https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_emails](https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_emails)) or [email domains]([https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_domains](https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#_app_console_whitelist_domains)) which users are allowed to signup from. You can choose one or multiple restriction methods to apply.
## Scaling
Appwrite was built with scalability in mind. Appwrite can potentially scale horizontally infinitely with no known limitations.
Appwrite uses a few containers to run, where each container has its job. Most of the Appwrite containers are stateless, and in order to scale them, all you need is run multiple instances of them and setup a load balancer in front of them.
If you decide to set up a load balancer for a specific container, make sure that the containers that are trying to communicate with it are accessing it through a load balancer and not directly. All connections between Appwrite different containers are set using Docker environment variables.
There are three Appwrite containers that do keep their state are the MariaDB, Redis, and InfluxDB containers that are used for storing data, cache, and stats (in this order). To scale them out, all you need to do is set up a standard cluster (same as you would with any other app using these technologies) according to your needs and performance.
## Sending Emails
Sending emails is hard. There are a lot of SPAM rules and configurations to master in order to set a functional SMTP server. The SMTP server that comes packaged with Appwrite is great for development but needs some work done to function well against SPAM filters. You can find some guidelines in this [tutorial]([https://www.digitalocean.com/community/tutorials/how-to-use-an-spf-record-to-prevent-spoofing-improve-e-mail-reliability](https://www.digitalocean.com/community/tutorials/how-to-use-an-spf-record-to-prevent-spoofing-improve-e-mail-reliability)).
Another **easier option** is to use an SMTP as a service product like [Sendgrid]([https://sendgrid.com/](https://sendgrid.com/)) or [Mailgun]([https://www.mailgun.com/](https://www.mailgun.com/)). You can change Appwrite SMTP settings and credentials to any 3rd party provider you like who support SMTP integration using our [Docker environment variables]([https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#smtp](https://github.com/appwrite/appwrite/blob/master/docs/tutorials/environment-variables.md#smtp)). Most services offer a decent free tier to get started with.
## Backups
Backups are highly recommended for any production environment. Currently, there is not built-in script we provide to do this automatically. To be able to backup your Appwrite server data, stats, and files you will need to do the following.
1. Create a script to backups and restore your MariaDB Appwrite schema. Note that trying to backup MariaDB using a docker volume backup can result in a corrupted copy of your data. It is recommended to use MariaDB or MySQL built-in tools for this.
2. Create a script to backups and restore your InfluxDB stats. If you dont care much about your server stats, you can skip this.
3. Create a script to backup Appwrite storage volume. There are many [online resources]([https://blog.ssdnodes.com/blog/docker-backup-volumes/](https://blog.ssdnodes.com/blog/docker-backup-volumes/)) explaining different ways to backup a docker volume. When running on multiple servers, it is very recommended to use an attachable storage point. Some cloud providers offer integrated backups to such attachable mount like GCP, AWS, DigitalOcean, and the list continues.

View file

@ -2212,9 +2212,11 @@ match=text.match(new RegExp(regex,'gi'))
if(!match){return fail}
for(i=0,len=match.length;i<len;i++){if(!process(match[i])){return fail}}
return(date.getTime()/1000)}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'string':value=value.toString();break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,to));return value;}
switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'string':value=value.toString();break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return value;}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
ref=json[name];}
else{if(!Array.isArray(json[name])){json[name]=[];}
json[name].push({});ref=json[name][json[name].length-1];}}

View file

@ -256,9 +256,11 @@ match=text.match(new RegExp(regex,'gi'))
if(!match){return fail}
for(i=0,len=match.length;i<len;i++){if(!process(match[i])){return fail}}
return(date.getTime()/1000)}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'string':value=value.toString();break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,to));return value;}
switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'string':value=value.toString();break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return value;}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
ref=json[name];}
else{if(!Array.isArray(json[name])){json[name]=[];}
json[name].push({});ref=json[name][json[name].length-1];}}

View file

@ -4,6 +4,10 @@
window.ls.container.set('form', function () {
function cast(value, to) {
if (value && Array.isArray(value) && to !== 'array') {
value = value.map(element => cast(element, to));
return value;
}
switch (to) {
case 'int':
case 'integer':
@ -42,6 +46,12 @@
let ref = json;
if (name && 'FORM' !== element.tagName) {
if (name.startsWith('[')) { // Check for array names
let splitName = name.split('.');
if (splitName.length > 1 && splitName[0].endsWith(']')) {
name = splitName[splitName.length-1];
}
}
if ('FIELDSET' === element.tagName) { // Fieldset Array / Object
if (castTo === 'object') {
@ -118,4 +128,4 @@
}
}, true, false);
})(window);
})(window);

View file

@ -28,11 +28,17 @@ class Auth
/**
* Token Types.
*/
const TOKEN_TYPE_LOGIN = 1;
const TOKEN_TYPE_LOGIN = 1; // Deprecated
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
/**
* Token Expiration times.
*/
@ -207,6 +213,29 @@ class Auth
return false;
}
/**
* Verify session and check that its not expired.
*
* @param array $sessions
* @param string $secret
*
* @return bool|string
*/
public static function sessionVerify(array $sessions, string $secret)
{
foreach ($sessions as $session) { /** @var Document $session */
if ($session->isSet('secret') &&
$session->isSet('expire') &&
$session->isSet('provider') &&
$session->getAttribute('secret') === self::hash($secret) &&
$session->getAttribute('expire') >= \time()) {
return (string)$session->getId();
}
}
return false;
}
/**
* Is Previligged User?
*

View file

@ -27,6 +27,7 @@ class Database
// Auth, Account and Users (private to user)
const SYSTEM_COLLECTION_USERS = 'users';
const SYSTEM_COLLECTION_SESSIONS = 'sessions';
const SYSTEM_COLLECTION_TOKENS = 'tokens';
// Teams (shared among team members)

View file

@ -75,11 +75,10 @@ abstract class Migration
]);
$sum = \count($all);
Runtime::setHookFlags(SWOOLE_HOOK_ALL);
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Console::log('Migrating: ' . $offset . ' / ' . $this->projectDB->getSum());
\Co\run(function () use ($all, $callback) {
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
foreach ($all as $document) {
go(function () use ($document, $callback) {

View file

@ -0,0 +1,70 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V07 extends Migration
{
public function execute(): void
{
$db = $this->db;
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
$providers = Config::getParam('providers');
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_USERS:
foreach ($providers as $key => $provider) {
/**
* Remove deprecated OAuth2 properties in the Users Documents.
*/
if (!empty($document->getAttribute('oauth2' . \ucfirst($key)))) {
$document->removeAttribute('oauth2' . \ucfirst($key));
}
if (!empty($document->getAttribute('oauth2' . \ucfirst($key) . 'AccessToken'))) {
$document->removeAttribute('oauth2' . \ucfirst($key) . 'AccessToken');
}
/**
* Invalidate all Login Tokens, since they can't be migrated to the new structure.
* Reason for it is the missing distinction between E-Mail and OAuth2 tokens.
*/
$tokens = array_filter($document->getAttribute('tokens', []), function($token) {
return ($token->getAttribute('type') != Auth::TOKEN_TYPE_LOGIN);
});
$document->setAttribute('tokens', array_values($tokens));
}
break;
}
foreach ($document as &$attr) { // Handle child documents
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* Email
*
* Validate that an variable is a valid email address
*
* @package Utopia\Validator
*/
class Email extends Validator
{
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription()
{
return 'Value must be a valid email address';
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType()
{
return 'string';
}
/**
* Is valid
*
* Validation will pass when $value is valid email address.
*
* @param mixed $value
* @return bool
*/
public function isValid($value)
{
if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* Host
*
* Validate that a host is allowed from given whitelisted hosts list
*
* @package Utopia\Validator
*/
class Host extends Validator
{
protected $whitelist = [];
/**
* @param array $whitelist
*/
public function __construct(array $whitelist)
{
$this->whitelist = $whitelist;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription()
{
return 'URL host must be one of: ' . \implode(', ', $this->whitelist);
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType()
{
return 'string';
}
/**
* Is valid
*
* Validation will pass when $value starts with one of the given hosts
*
* @param mixed $value
* @return bool
*/
public function isValid($value)
{
$urlValidator = new URL();
if (!$urlValidator->isValid($value)) {
return false;
}
if (\in_array(\parse_url($value, PHP_URL_HOST), $this->whitelist)) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Appwrite\Network\Validator;
use Exception;
use Utopia\Validator;
/**
* IP
*
* Validate that an variable is a valid IP address
*
* @package Utopia\Validator
*/
class IP extends Validator
{
const ALL = 'all';
const V4 = 'ipv4';
const V6 = 'ipv6';
/**
* @var string
*/
protected $type = self::ALL;
/**
* Constructor
*
* Set a the type of IP check.
*
* @param string $type
*/
public function __construct(string $type = self::ALL)
{
if (!in_array($type, [self::ALL, self::V4, self::V6])) {
throw new Exception('Unsupported IP type');
}
$this->type = $type;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription()
{
return 'Value must be a valid IP address';
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType()
{
return 'string';
}
/**
* Is valid
*
* Validation will pass when $value is valid IP address.
*
* @param mixed $value
* @return bool
*/
public function isValid($value)
{
switch ($this->type) {
case self::ALL:
if (\filter_var($value, FILTER_VALIDATE_IP)) {
return true;
}
break;
case self::V4:
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return true;
}
break;
case self::V6:
if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return true;
}
break;
default:
return false;
break;
}
return false;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Appwrite\Network\Validator;
use Utopia\Validator;
/**
* URL
*
* Validate that an variable is a valid URL
*
* @package Utopia\Validator
*/
class URL extends Validator
{
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription()
{
return 'Value must be a valid URL';
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType()
{
return 'string';
}
/**
* Is valid
*
* Validation will pass when $value is valid URL.
*
* @param mixed $value
* @return bool
*/
public function isValid($value)
{
if (\filter_var($value, FILTER_VALIDATE_URL) === false) {
return false;
}
return true;
}
}

View file

@ -87,7 +87,9 @@ class OpenAPI3 extends Format
$output['components']['securitySchemes']['Mode']['x-appwrite'] = ['demo' => ''];
}
foreach ($this->routes as $route) { /* @var $route \Utopia\Route */
$usedModels = [];
foreach ($this->routes as $route) { /** @var \Utopia\Route $route */
$url = \str_replace('/v1', '', $route->getURL());
$scope = $route->getLabel('scope', '');
$hide = $route->getLabel('sdk.hide', false);
@ -150,6 +152,7 @@ class OpenAPI3 extends Format
// ],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
@ -249,7 +252,7 @@ class OpenAPI3 extends Format
$node['schema']['format'] = 'format';
$node['schema']['x-example'] = 'password';
break;
case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */
case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */
$node['schema']['type'] = 'integer';
$node['schema']['format'] = 'int32';
$node['schema']['x-example'] = $validator->getMin();
@ -266,7 +269,7 @@ class OpenAPI3 extends Format
$node['schema']['format'] = 'url';
$node['schema']['x-example'] = 'https://example.com';
break;
case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */
case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */
$node['schema']['type'] = 'string';
$node['schema']['x-example'] = $validator->getList()[0];
break;
@ -320,8 +323,18 @@ class OpenAPI3 extends Format
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
}
foreach ($this->models as $model) {
foreach ($model->getRules() as $rule) {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
}
}
}
foreach ($this->models as $model) {
if (!in_array($model->getType(), $usedModels) && $model->getType() !== 'error') {
continue;
}
$required = $model->getRequired();
$rules = $model->getRules();

View file

@ -4,6 +4,7 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use stdClass;
class Swagger2 extends Format
@ -85,7 +86,9 @@ class Swagger2 extends Format
$output['securityDefinitions']['Mode']['x-appwrite'] = ['demo' => ''];
}
foreach ($this->routes as $route) { /* @var $route \Utopia\Route */
$usedModels = [];
foreach ($this->routes as $route) { /** @var \Utopia\Route $route */
$url = \str_replace('/v1', '', $route->getURL());
$scope = $route->getLabel('scope', '');
$hide = $route->getLabel('sdk.hide', false);
@ -152,6 +155,7 @@ class Swagger2 extends Format
],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'schema' => [
@ -245,7 +249,7 @@ class Swagger2 extends Format
$node['format'] = 'format';
$node['x-example'] = 'password';
break;
case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */
case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */
$node['type'] = 'integer';
$node['format'] = 'int32';
$node['x-example'] = $validator->getMin();
@ -262,7 +266,7 @@ class Swagger2 extends Format
$node['format'] = 'url';
$node['x-example'] = 'https://example.com';
break;
case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */
case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */
$node['type'] = 'string';
$node['x-example'] = $validator->getList()[0];
break;
@ -321,8 +325,18 @@ class Swagger2 extends Format
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
}
foreach ($this->models as $model) {
foreach ($model->getRules() as $rule) {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
}
}
}
foreach ($this->models as $model) {
if (!in_array($model->getType(), $usedModels) && $model->getType() !== 'error') {
continue;
}
$required = $model->getRequired();
$rules = $model->getRules();

View file

@ -15,6 +15,7 @@ use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
use Appwrite\Utopia\Response\Model\Currency;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Domain;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
@ -61,6 +62,7 @@ class Response extends SwooleResponse
const MODEL_COLLECTION = 'collection';
const MODEL_COLLECTION_LIST = 'collectionList';
const MODEL_RULE = 'rule';
const MODEL_DOCUMENT = 'document';
const MODEL_DOCUMENT_LIST = 'documentList';
// Users
@ -145,7 +147,7 @@ class Response extends SwooleResponse
->setModel(new ErrorDev())
// Lists
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_ANY))
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION))
->setModel(new BaseList('Logs List', self::MODEL_LOG_LIST, 'logs', self::MODEL_LOG, false))
@ -169,6 +171,7 @@ class Response extends SwooleResponse
// Entities
->setModel(new Permissions())
->setModel(new Collection())
->setModel(new ModelDocument())
->setModel(new Rule())
->setModel(new Log())
->setModel(new User())

View file

@ -108,6 +108,7 @@ class V06 extends Filter {
break;
case Response::MODEL_ANY :
case Response::MODEL_DOCUMENT :
$parsedResponse = $content;
break;

View file

@ -0,0 +1,52 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class Document extends Any
{
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Document';
}
/**
* Get Collection
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Document ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$collection', [
'type' => self::TYPE_STRING,
'description' => 'Collection ID.',
'default' => '',
'example' => '5e5ea5c15117e',
])
->addRule('$permissions', [
'type' => Response::MODEL_PERMISSIONS,
'description' => 'Document permissions.',
'default' => new \stdClass,
'example' => new \stdClass,
'array' => false,
]);
}
}

View file

@ -28,6 +28,24 @@ class Session extends Model
'default' => 0,
'example' => 1592981250,
])
->addRule('provider', [
'type' => self::TYPE_STRING,
'description' => 'Session Provider.',
'default' => '',
'example' => 'email',
])
->addRule('providerUid', [
'type' => self::TYPE_STRING,
'description' => 'Session Provider User ID.',
'default' => '',
'example' => 'user@example.com',
])
->addRule('providerToken', [
'type' => self::TYPE_STRING,
'description' => 'Session Provider Token.',
'default' => '',
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
])
->addRule('ip', [
'type' => self::TYPE_STRING,
'description' => 'IP in use when the session was created.',

View file

@ -485,6 +485,14 @@ class FunctionsCustomServerTest extends Scope
'command' => 'python main.py',
'timeout' => 15,
],
[
'language' => 'Python',
'version' => '3.9',
'name' => 'python-3.9',
'code' => $functions.'/python.tar.gz',
'command' => 'python main.py',
'timeout' => 15,
],
[
'language' => 'Node.js',
'version' => '14.5',
@ -533,6 +541,14 @@ class FunctionsCustomServerTest extends Scope
'command' => 'deno run --allow-env index.ts',
'timeout' => 15,
],
[
'language' => 'Deno',
'version' => '1.8',
'name' => 'deno-1.8',
'code' => $functions.'/deno.tar.gz',
'command' => 'deno run --allow-env index.ts',
'timeout' => 15,
],
[
'language' => 'Dart',
'version' => '2.10',
@ -541,6 +557,14 @@ class FunctionsCustomServerTest extends Scope
'command' => 'dart main.dart',
'timeout' => 15,
],
[
'language' => 'Dart',
'version' => '2.12',
'name' => 'dart-2.12',
'code' => $functions.'/dart.tar.gz',
'command' => 'dart main.dart',
'timeout' => 15,
],
[
'language' => '.NET',
'version' => '3.1',

View file

@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Storage;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Image\Image;
trait StorageBase
{
@ -73,25 +74,75 @@ trait StorageBase
$this->assertEquals(200, $file2['headers']['status-code']);
$this->assertEquals('image/png', $file2['headers']['content-type']);
$this->assertNotEmpty($file2['body']);
$file3 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/download', array_merge([
//new image preview features
$file3 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'width' => 300,
'height' => 100,
'borderRadius' => '50',
'opacity' => '0.5',
'output' => 'png',
'rotation' => '45',
]);
$this->assertEquals(200, $file3['headers']['status-code']);
$this->assertEquals('attachment; filename="logo.png"', $file3['headers']['content-disposition']);
$this->assertEquals('image/png', $file3['headers']['content-type']);
$this->assertNotEmpty($file3['body']);
$file4 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/view', array_merge([
$image = new \Imagick();
$image->readImageBlob($file3['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
$file4 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'width' => 200,
'height' => 80,
'borderWidth' => '5',
'borderColor' => 'ff0000',
'output' => 'jpg',
]);
$this->assertEquals(200, $file4['headers']['status-code']);
$this->assertEquals('image/jpeg', $file4['headers']['content-type']);
$this->assertNotEmpty($file4['body']);
$image = new \Imagick();
$image->readImageBlob($file4['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.jpg');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('JPEG', $image->getImageFormat());
$file5 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file4['headers']['status-code']);
$this->assertEquals('image/png', $file4['headers']['content-type']);
$this->assertNotEmpty($file4['body']);
$this->assertEquals(200, $file5['headers']['status-code']);
$this->assertEquals('attachment; filename="logo.png"', $file5['headers']['content-disposition']);
$this->assertEquals('image/png', $file5['headers']['content-type']);
$this->assertNotEmpty($file5['body']);
$file6 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/view', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file6['headers']['status-code']);
$this->assertEquals('image/png', $file6['headers']['content-type']);
$this->assertNotEmpty($file6['body']);
/**
* Test for FAILURE

View file

@ -62,7 +62,7 @@ services:
depends_on:
- mariadb
- redis
- clamav
# - clamav
- influxdb
environment:
- _APP_ENV
@ -81,6 +81,7 @@ services:
- _APP_USAGE_STATS
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_STORAGE_ANTIVIRUS=disabled
- _APP_STORAGE_LIMIT
- _APP_FUNCTIONS_TIMEOUT
- _APP_FUNCTIONS_CONTAINERS
@ -326,14 +327,14 @@ services:
volumes:
- appwrite-redis:/data:rw
clamav:
image: appwrite/clamav:1.2.0
container_name: appwrite-clamav
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads
# clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# restart: unless-stopped
# networks:
# - appwrite
# volumes:
# - appwrite-uploads:/storage/uploads
influxdb:
image: influxdb:1.6

BIN
tests/resources/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -22,5 +22,5 @@ void main() { // Init SDK
print(Platform.environment["APPWRITE_FUNCTION_ENV_VERSION"]);
// print(result['$id']);
print(Platform.environment["APPWRITE_FUNCTION_EVENT"]);
print(Platform.environment["APPWRITE_FUNCTION_EVENT_PAYLOAD"]);
print(Platform.environment["APPWRITE_FUNCTION_EVENT_DATA"]);
}

View file

@ -20,4 +20,4 @@ console.log(Deno.env.get("APPWRITE_FUNCTION_ENV_NAME") || '');
console.log(Deno.env.get("APPWRITE_FUNCTION_ENV_VERSION") || '');
// console.log(result['$id']"));
console.log(Deno.env.get("APPWRITE_FUNCTION_EVENT") || '');
console.log(Deno.env.get("APPWRITE_FUNCTION_EVENT_PAYLOAD") || '');
console.log(Deno.env.get("APPWRITE_FUNCTION_EVENT_DATA") || '');

View file

@ -22,7 +22,7 @@ namespace dotnet
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_ENV_NAME"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_ENV_VERSION"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT_PAYLOAD"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT_DATA"));
}
}
}

View file

@ -22,7 +22,7 @@ namespace dotnet
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_ENV_NAME"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_ENV_VERSION"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT_PAYLOAD"));
Console.WriteLine(Environment.GetEnvironmentVariable("APPWRITE_FUNCTION_EVENT_DATA"));
}
}
}

View file

@ -20,4 +20,4 @@ console.log(process.env.APPWRITE_FUNCTION_ENV_NAME);
console.log(process.env.APPWRITE_FUNCTION_ENV_VERSION);
// console.log(result['$id']);
console.log(process.env.APPWRITE_FUNCTION_EVENT);
console.log(process.env.APPWRITE_FUNCTION_EVENT_PAYLOAD);
console.log(process.env.APPWRITE_FUNCTION_EVENT_DATA);

View file

@ -25,7 +25,7 @@ echo $_ENV['APPWRITE_FUNCTION_ENV_NAME']."\n";
echo $_ENV['APPWRITE_FUNCTION_ENV_VERSION']."\n";
// echo $result['$id'];
echo $_ENV['APPWRITE_FUNCTION_EVENT']."\n";
echo $_ENV['APPWRITE_FUNCTION_EVENT_PAYLOAD']."\n";
echo $_ENV['APPWRITE_FUNCTION_EVENT_DATA']."\n";
echo 'data:'.$_ENV['APPWRITE_FUNCTION_DATA']."\n";
echo 'userId:'.$_ENV['APPWRITE_FUNCTION_USER_ID']."\n";
echo 'jwt:'.$_ENV['APPWRITE_FUNCTION_JWT']."\n";

Binary file not shown.

View file

@ -25,4 +25,4 @@ echo $_ENV['APPWRITE_FUNCTION_ENV_NAME']."\n";
echo $_ENV['APPWRITE_FUNCTION_ENV_VERSION']."\n";
// echo $result['$id'];
echo $_ENV['APPWRITE_FUNCTION_EVENT']."\n";
echo $_ENV['APPWRITE_FUNCTION_EVENT_PAYLOAD']."\n";
echo $_ENV['APPWRITE_FUNCTION_EVENT_DATA']."\n";

View file

@ -20,4 +20,4 @@ print(os.environ["APPWRITE_FUNCTION_ENV_NAME"])
print(os.environ["APPWRITE_FUNCTION_ENV_VERSION"])
# print(result["$id"])
print(os.environ["APPWRITE_FUNCTION_EVENT"])
print(os.environ["APPWRITE_FUNCTION_EVENT_PAYLOAD"])
print(os.environ["APPWRITE_FUNCTION_EVENT_DATA"])

View file

@ -20,4 +20,4 @@ puts ENV["APPWRITE_FUNCTION_ENV_NAME"]
puts ENV["APPWRITE_FUNCTION_ENV_VERSION"]
# puts result["$id"]
puts ENV["APPWRITE_FUNCTION_EVENT"]
puts ENV["APPWRITE_FUNCTION_EVENT_PAYLOAD"]
puts ENV["APPWRITE_FUNCTION_EVENT_DATA"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -62,41 +62,55 @@ class AuthTest extends TestCase
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
}
public function testTokenVerify()
public function testSessionVerify()
{
$secret = 'secret1';
$hash = Auth::hash($secret);
$tokens1 = [
new Document([
'$id' => 'token1',
'type' => Auth::TOKEN_TYPE_LOGIN,
'expire' => time() + 60 * 60 * 24,
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
new Document([
'$id' => 'token2',
'type' => Auth::TOKEN_TYPE_LOGIN,
'expire' => time() - 60 * 60 * 24,
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
];
$tokens2 = [
new Document([ // Correct secret and type time, wrong expire time
'$id' => 'token1',
'type' => Auth::TOKEN_TYPE_LOGIN,
'expire' => time() - 60 * 60 * 24,
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
new Document([
'$id' => 'token2',
'type' => Auth::TOKEN_TYPE_LOGIN,
'expire' => time() - 60 * 60 * 24,
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
];
$tokens3 = [ // Correct secret and expire time, wrong type
$this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1');
$this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false);
$this->assertEquals(Auth::sessionVerify($tokens2, $secret), false);
$this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false);
}
public function testTokenVerify()
{
$secret = 'secret1';
$hash = Auth::hash($secret);
$tokens1 = [
new Document([
'$id' => 'token1',
'type' => Auth::TOKEN_TYPE_RECOVERY,
@ -105,20 +119,51 @@ class AuthTest extends TestCase
]),
new Document([
'$id' => 'token2',
'type' => Auth::TOKEN_TYPE_LOGIN,
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => time() - 60 * 60 * 24,
'secret' => 'secret2',
]),
];
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_LOGIN, $secret), 'token1');
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_LOGIN, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_LOGIN, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
$tokens2 = [
new Document([ // Correct secret and type time, wrong expire time
'$id' => 'token1',
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => time() - 60 * 60 * 24,
'secret' => $hash,
]),
new Document([
'$id' => 'token2',
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => time() - 60 * 60 * 24,
'secret' => 'secret2',
]),
];
$tokens3 = [ // Correct secret and expire time, wrong type
new Document([
'$id' => 'token1',
'type' => Auth::TOKEN_TYPE_INVITE,
'expire' => time() + 60 * 60 * 24,
'secret' => $hash,
]),
new Document([
'$id' => 'token2',
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => time() - 60 * 60 * 24,
'secret' => 'secret2',
]),
];
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), 'token1');
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
}
public function testIsPreviliggedUser()
{
$this->assertEquals(false, Auth::isPreviliggedUser([]));

View file

@ -36,7 +36,7 @@ class ComposeTest extends TestCase
public function testServices()
{
$this->assertCount(17, $this->object->getServices());
$this->assertCount(16, $this->object->getServices());
$this->assertEquals('appwrite-telegraf', $this->object->getService('telegraf')->getContainerName());
$this->assertEquals('appwrite', $this->object->getService('appwrite')->getContainerName());
$this->assertEquals('', $this->object->getService('appwrite')->getImageVersion());

View file

@ -0,0 +1,36 @@
<?php
namespace Appwrite\Tests;
use PHPUnit\Framework\TestCase;
class CollectionsTest extends TestCase
{
protected $collections;
public function setUp(): void
{
$this->collections = require('app/config/collections.php');
}
public function tearDown(): void
{
}
public function testDuplicateRules()
{
foreach ($this->collections as $collection) {
if ($collection['rules']) {
foreach ($collection['rules'] as $check) {
$occurences = 0;
foreach ($collection['rules'] as $rule) {
if ($rule['key'] == $check['key']) {
$occurences++;
}
}
$this->assertEquals(1, $occurences);
}
}
}
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Migration\Version\V07;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Auth\Auth;
use Utopia\Config\Config;
class MigrationV07Test extends MigrationTest
{
public function setUp(): void
{
Config::load('providers', __DIR__ . '/../../../app/config/providers.php');
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V07($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V07');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'oauth2Github' => 123,
'oauth2GithubAccessToken' => 456,
'tokens' => [
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_LOGIN,
'secret' => 'login',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_INVITE,
'secret' => 'invite',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_RECOVERY,
'secret' => 'recovery',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_VERIFICATION,
'secret' => 'verification',
]),
]
]));
$this->assertEquals($document->getAttribute('oauth2Github', null), null);
$this->assertEquals($document->getAttribute('oauth2GithubAccessToken', null), null);
$this->assertCount(3, $document->getAttribute('tokens', []));
$this->assertEquals(Auth::TOKEN_TYPE_INVITE, $document->getAttribute('tokens', [])[0]['type']);
$this->assertEquals(Auth::TOKEN_TYPE_RECOVERY, $document->getAttribute('tokens', [])[1]['type']);
$this->assertEquals(Auth::TOKEN_TYPE_VERIFICATION, $document->getAttribute('tokens', [])[2]['type']);
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
namespace Appwrite\Network\Validator;
use PHPUnit\Framework\TestCase;
class EmailTest extends TestCase
{
/**
* @var Email
*/
protected $email = null;
public function setUp():void
{
$this->email = new Email();
}
public function tearDown():void
{
$this->email = null;
}
public function testIsValid()
{
// Assertions
$this->assertEquals(true, $this->email->isValid('email@domain.com'));
$this->assertEquals(true, $this->email->isValid('firstname.lastname@domain.com'));
$this->assertEquals(true, $this->email->isValid('email@subdomain.domain.com'));
$this->assertEquals(true, $this->email->isValid('firstname+lastname@domain.com'));
$this->assertEquals(true, $this->email->isValid('email@[123.123.123.123]'));
$this->assertEquals(true, $this->email->isValid('"email"@domain.com'));
$this->assertEquals(true, $this->email->isValid('1234567890@domain.com'));
$this->assertEquals(true, $this->email->isValid('email@domain-one.com'));
$this->assertEquals(true, $this->email->isValid('_______@domain.com'));
$this->assertEquals(true, $this->email->isValid('email@domain.name'));
$this->assertEquals(true, $this->email->isValid('email@domain.co.jp'));
$this->assertEquals(true, $this->email->isValid('firstname-lastname@domain.com'));
$this->assertEquals(false, $this->email->isValid(false));
$this->assertEquals(false, $this->email->isValid(['string', 'string']));
$this->assertEquals(false, $this->email->isValid(1));
$this->assertEquals(false, $this->email->isValid(1.2));
$this->assertEquals(false, $this->email->isValid('plainaddress')); // Missing @ sign and domain
$this->assertEquals(false, $this->email->isValid('@domain.com')); // Missing username
$this->assertEquals(false, $this->email->isValid('#@%^%#$@#$@#.com')); // Garbage
$this->assertEquals(false, $this->email->isValid('Joe Smith <email@domain.com>')); // Encoded html within email is invalid
$this->assertEquals(false, $this->email->isValid('email.domain.com')); // Missing @
$this->assertEquals(false, $this->email->isValid('email@domain@domain.com')); // Two @ sign
$this->assertEquals(false, $this->email->isValid('.email@domain.com')); // Leading dot in address is not allowed
$this->assertEquals(false, $this->email->isValid('email.@domain.com')); // Trailing dot in address is not allowed
$this->assertEquals(false, $this->email->isValid('email..email@domain.com')); // Multiple dots
$this->assertEquals(false, $this->email->isValid('あいうえお@domain.com')); // Unicode char as address
$this->assertEquals(false, $this->email->isValid('email@domain.com (Joe Smith)')); // Text followed email is not allowed
$this->assertEquals(false, $this->email->isValid('email@domain')); // Missing top level domain (.com/.net/.org/etc)
$this->assertEquals(false, $this->email->isValid('email@-domain.com')); // Leading dash in front of domain is invalid
$this->assertEquals(false, $this->email->isValid('email@111.222.333.44444')); // Invalid IP format
$this->assertEquals(false, $this->email->isValid('email@domain..com')); // Multiple dot in the domain portion is invalid
$this->assertEquals($this->email->getType(), 'string');
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
namespace Appwrite\Network\Validator;
use PHPUnit\Framework\TestCase;
class HostTest extends TestCase
{
/**
* @var Host
*/
protected $host = null;
public function setUp():void
{
$this->host = new Host(['appwrite.io', 'subdomain.appwrite.test', 'localhost']);
}
public function tearDown():void
{
$this->host = null;
}
public function testIsValid()
{
// Assertions
$this->assertEquals($this->host->isValid('https://appwrite.io/link'), true);
$this->assertEquals($this->host->isValid('https://localhost'), true);
$this->assertEquals($this->host->isValid('localhost'), false);
$this->assertEquals($this->host->isValid('http://subdomain.appwrite.test/path'), true);
$this->assertEquals($this->host->isValid('http://test.subdomain.appwrite.test/path'), false);
$this->assertEquals($this->host->getType(), 'string');
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
namespace Appwrite\Network\Validator;
use PHPUnit\Framework\TestCase;
class IPTest extends TestCase
{
public function tearDown():void
{
$this->validator = null;
}
public function testIsValidIP()
{
$validator = new IP();
// Assertions
$this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true);
$this->assertEquals($validator->isValid('109.67.204.101'), true);
$this->assertEquals($validator->isValid(23.5), false);
$this->assertEquals($validator->isValid('23.5'), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(true), false);
$this->assertEquals($validator->isValid(false), false);
$this->assertEquals($validator->getType(), 'string');
}
public function testIsValidIPALL()
{
$validator = new IP(IP::ALL);
// Assertions
$this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true);
$this->assertEquals($validator->isValid('109.67.204.101'), true);
$this->assertEquals($validator->isValid(23.5), false);
$this->assertEquals($validator->isValid('23.5'), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(true), false);
$this->assertEquals($validator->isValid(false), false);
}
public function testIsValidIPV4()
{
$validator = new IP(IP::V4);
// Assertions
$this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), false);
$this->assertEquals($validator->isValid('109.67.204.101'), true);
$this->assertEquals($validator->isValid(23.5), false);
$this->assertEquals($validator->isValid('23.5'), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(true), false);
$this->assertEquals($validator->isValid(false), false);
}
public function testIsValidIPV6()
{
$validator = new IP(IP::V6);
// Assertions
$this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true);
$this->assertEquals($validator->isValid('109.67.204.101'), false);
$this->assertEquals($validator->isValid(23.5), false);
$this->assertEquals($validator->isValid('23.5'), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(true), false);
$this->assertEquals($validator->isValid(false), false);
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* Utopia PHP Framework
*
* @package Framework
* @subpackage Tests
*
* @link https://github.com/utopia-php/framework
* @author Appwrite Team <team@appwrite.io>
* @version 1.0 RC4
* @license The MIT License (MIT) <http://www.opensource.org/licenses/mit-license.php>
*/
namespace Appwrite\Network\Validator;
use PHPUnit\Framework\TestCase;
class URLTest extends TestCase
{
/**
* @var Domain
*/
protected $url = null;
public function setUp():void
{
$this->url = new URL();
}
public function tearDown():void
{
$this->url = null;
}
public function testIsValid()
{
// Assertions
$this->assertEquals(true, $this->url->isValid('http://example.com'));
$this->assertEquals(true, $this->url->isValid('https://example.com'));
$this->assertEquals(true, $this->url->isValid('htts://example.com')); // does not validate protocol
$this->assertEquals(false, $this->url->isValid('example.com')); // though, requires some kind of protocol
$this->assertEquals(false, $this->url->isValid('http:/example.com'));
$this->assertEquals(true, $this->url->isValid('http://exa-mple.com'));
$this->assertEquals(false, $this->url->isValid('htt@s://example.com'));
$this->assertEquals(true, $this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar'));
$this->assertEquals(true, $this->url->isValid('http://www.example.com/?q=%3Casdf%3E'));
}
}