1
0
Fork 0
mirror of synced 2024-06-28 19:20:25 +12:00

Merge branch '1.3.x' into feat-reset-sdk-account-model

This commit is contained in:
Damodar Lohani 2023-03-09 07:06:24 +05:45 committed by GitHub
commit ad8ae92a6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
238 changed files with 11320 additions and 403 deletions

View file

@ -1,6 +1,13 @@
# Version TBD
- Fix expire to formatTz in create account session [#4985](https://github.com/appwrite/appwrite/pull/4985)
## Features
- Password dictionary setting allows to compare user's password against command password database [4906](https://github.com/appwrite/appwrite/pull/4906)
- Password history setting allows to save user's last used password so that it may not be used again. Maximum number of history saved is 20, which can be configured. Minimum is 0 which means disabled. [#4866](https://github.com/appwrite/appwrite/pull/4866)
- Update APIs to check X-Appwrite-Timestamp header [#5024](https://github.com/appwrite/appwrite/pull/5024)
## Bugs
- Fix not storing function's response on response codes 5xx [#4610](https://github.com/appwrite/appwrite/pull/4610)
- Fix expire to formatTz in create account session [#4985](https://github.com/appwrite/appwrite/pull/4985)
# Version 1.2.1
## Changes

File diff suppressed because it is too large Load diff

View file

@ -1234,6 +1234,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('passwordHistory'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('password'),
'type' => Database::VAR_STRING,

View file

@ -408,6 +408,11 @@ return [
'description' => 'Document with the requested ID already exists.',
'code' => 409,
],
Exception::DOCUMENT_UPDATE_CONFLICT => [
'name' => Exception::DOCUMENT_UPDATE_CONFLICT,
'description' => 'Remote document is newer than local.',
'code' => 409,
],
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [

View file

@ -11,7 +11,7 @@ return [
'description' => 'Client libraries for integrating with Appwrite to build client-based applications and websites. Read the [getting started for web](/docs/getting-started-for-web) or [getting started for Flutter](/docs/getting-started-for-flutter) tutorials to start building your first application.',
'enabled' => true,
'beta' => false,
'languages' => [ // TODO change key to 'sdks'
'sdks' => [
[
'key' => 'web',
'name' => 'Web',
@ -181,11 +181,11 @@ return [
'name' => 'Console',
'enabled' => false,
'beta' => false,
'languages' => [
'sdks' => [
[
'key' => 'web',
'name' => 'Console',
'version' => '7.2.0',
'version' => '0.0.2-preview-0.0',
'url' => 'https://github.com/appwrite/sdk-for-console',
'package' => '',
'enabled' => true,
@ -193,10 +193,10 @@ return [
'dev' => false,
'hidden' => true,
'family' => APP_PLATFORM_CONSOLE,
'prism' => 'console',
'prism' => 'javascript',
'source' => \realpath(__DIR__ . '/../sdks/console-web'),
'gitUrl' => '',
'gitBranch' => '',
'gitUrl' => 'git@github.com:appwrite/sdk-for-console.git',
'gitBranch' => 'main',
'gitRepoName' => 'sdk-for-console',
'gitUserName' => 'appwrite',
],
@ -227,7 +227,7 @@ return [
'description' => 'Libraries for integrating with Appwrite to build server side integrations. Read the [getting started for server](/docs/getting-started-for-server) tutorial to start building your first server integration.',
'enabled' => true,
'beta' => false,
'languages' => [
'sdks' => [
[
'key' => 'nodejs',
'name' => 'Node.js',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -40,6 +40,8 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PasswordDictionary;
$oauthDefaultSuccess = '/auth/oauth2/success';
$oauthDefaultFailure = '/auth/oauth2/failure';
@ -64,7 +66,7 @@ App::post('/v1/account')
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
->inject('response')
@ -72,7 +74,6 @@ App::post('/v1/account')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
@ -97,6 +98,8 @@ App::post('/v1/account')
}
}
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
try {
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
@ -109,10 +112,11 @@ App::post('/v1/account')
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'password' => $password,
'passwordHistory' => $passwordHistory > 0 ? [$password] : [],
'passwordUpdate' => DateTime::now(),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => DateTime::now(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@ -501,8 +505,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
}
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
try {
$userId = ID::unique();
$password = Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
@ -513,7 +520,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'email' => $email,
'emailVerification' => true,
'status' => true, // Email should already be authenticated by OAuth2 provider
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'passwordHistory' => $passwordHistory > 0 ? [$password] : null,
'password' => $password,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => null,
@ -1516,15 +1524,18 @@ App::patch('/v1/account/name')
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $name, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $name, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user
$user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')])));
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email', ''), $user->getAttribute('phone', '')]));
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1546,27 +1557,46 @@ App::patch('/v1/account/password')
->label('sdk.description', '/docs/references/account/update-password.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) {
// Check old password only if its an existing user.
if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now()));
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = [];
if ($historyLimit > 0) {
$history = $user->getAttribute('passwordHistory', []);
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED, 'The password was recently used', 409);
}
$history[] = $newPassword;
array_slice($history, (count($history) - $historyLimit), $historyLimit);
}
$user
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1593,11 +1623,12 @@ App::patch('/v1/account/email')
->label('sdk.offline.key', 'current')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $email, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
@ -1618,7 +1649,7 @@ App::patch('/v1/account/email')
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -1648,11 +1679,12 @@ App::patch('/v1/account/phone')
->label('sdk.offline.key', 'current')
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
@ -1669,7 +1701,7 @@ App::patch('/v1/account/phone')
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $user->getAttribute('email', ''), $phone]));
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -1698,13 +1730,16 @@ App::patch('/v1/account/prefs')
->label('sdk.offline.model', '/account/prefs')
->label('sdk.offline.key', 'current')
->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (array $prefs, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (array $prefs, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$user->setAttribute('prefs', $prefs);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events->setParam('userId', $user->getId());
@ -1726,15 +1761,17 @@ App::patch('/v1/account/status')
->label('sdk.description', '/docs/references/account/update-status.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->inject('request')
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('events')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events) {
->action(function (?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $events) {
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
$user->setAttribute('status', false);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$events
->setParam('userId', $user->getId())
@ -1764,6 +1801,7 @@ App::delete('/v1/account/sessions/:sessionId')
->label('sdk.response.model', Response::MODEL_NONE)
->label('abuse-limit', 100)
->param('sessionId', '', new UID(), 'Session ID. Use the string \'current\' to delete the current device session.')
->inject('requestTimestamp')
->inject('request')
->inject('response')
->inject('user')
@ -1771,7 +1809,7 @@ App::delete('/v1/account/sessions/:sessionId')
->inject('locale')
->inject('events')
->inject('project')
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Document $project) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $events, Document $project) {
$protocol = $request->getProtocol();
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
@ -1783,9 +1821,11 @@ App::delete('/v1/account/sessions/:sessionId')
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
unset($sessions[$key]);
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $session) {
return $dbForProject->deleteDocument('sessions', $session->getId());
});
$dbForProject->deleteDocument('sessions', $session->getId());
unset($sessions[$key]);
$session->setAttribute('current', false);
@ -2146,9 +2186,9 @@ App::put('/v1/account/recovery')
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('emailVerification', true));
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);

View file

@ -2007,7 +2007,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
// Validate queries
$queriesValidator = new Documents($collection->getAttribute('attributes'), $collection->getAttribute('indexes'));
$queriesValidator = new Documents($collection->getAttribute('attributes'));
$validQueries = $queriesValidator->isValid($queries);
if (!$validQueries) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $queriesValidator->getDescription());
@ -2252,11 +2252,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->param('documentId', '', new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Event $events, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@ -2286,6 +2287,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
}
// Read permission should not be required for update
/** @var Document */
$document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
if ($document->isEmpty()) {
@ -2331,10 +2333,31 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$data['$permissions'] = $permissions;
try {
$privateCollectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId();
if ($documentSecurity && !$valid) {
$document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data));
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
function () use ($dbForProject, $privateCollectionId, $document, $data) {
return $dbForProject->updateDocument(
$privateCollectionId,
$document->getId(),
new Document($data)
);
}
);
} else {
$document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)));
$document = Authorization::skip(function () use ($dbForProject, $requestTimestamp, $privateCollectionId, $document, $data) {
return $dbForProject->withRequestTimestamp(
$requestTimestamp,
function () use ($dbForProject, $privateCollectionId, $document, $data) {
return $dbForProject->updateDocument(
$privateCollectionId,
$document->getId(),
new Document($data)
);
}
);
});
}
/**
@ -2385,12 +2408,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events, Delete $deletes, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -2420,17 +2444,25 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$privateCollectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId();
if ($documentSecurity && !$valid) {
try {
$dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $privateCollectionId, $documentId) {
return $dbForProject->deleteDocument($privateCollectionId, $documentId);
});
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
} else {
Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
Authorization::skip(function () use ($dbForProject, $requestTimestamp, $privateCollectionId, $documentId) {
return $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $privateCollectionId, $documentId) {
return $dbForProject->deleteDocument($privateCollectionId, $documentId);
});
});
}
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$dbForProject->deleteCachedDocument($privateCollectionId, $documentId);
/**
* Reset $collection attribute to remove prefix.

View file

@ -80,7 +80,7 @@ App::post('/v1/projects')
}
$auth = Config::getParam('auth', []);
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
foreach ($auth as $index => $method) {
$auths[$method['key'] ?? ''] = true;
}
@ -575,6 +575,68 @@ App::patch('/v1/projects/:projectId/auth/:method')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/password-history')
->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateAuthPasswordHistory')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('limit', 0, new Range(0, APP_LIMIT_USER_PASSWORD_HISTORY), 'Set the max number of passwords to store in user history. User can\'t choose a new password that is already stored in the password history list. Max number of passwords allowed in history is' . APP_LIMIT_USER_PASSWORD_HISTORY . '. Default value is 0')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, int $limit, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['passwordHistory'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/password-dictionary')
->desc('Update authentication password disctionary status. Use this endpoint to enable or disable the dicitonary check for user password')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateAuthPasswordDictionary')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('enabled', false, new Boolean(false), 'Set whether or not to enable checking user\'s password against most commonly used passwords. Default is false.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['passwordDictionary'] = $enabled;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/max-sessions')
->desc('Update Project user sessions limit')
->groups(['api', 'projects'])

View file

@ -1,10 +1,12 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -20,7 +22,6 @@ use Appwrite\Utopia\Response;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -36,9 +37,7 @@ use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
->desc('Create Team')
@ -217,10 +216,11 @@ App::put('/v1/teams/:teamId')
->label('sdk.offline.key', '{teamId}')
->param('teamId', '', new UID(), 'Team ID.')
->param('name', null, new Text(128), 'New team name. Max length: 128 chars.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $teamId, string $name, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -228,9 +228,13 @@ App::put('/v1/teams/:teamId')
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$team = $dbForProject->updateDocument('teams', $team->getId(), $team
$team
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$teamId, $name])));
->setAttribute('search', implode(' ', [$teamId, $name]));
$team = $dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $team) {
return $dbForProject->updateDocument('teams', $team->getId(), $team);
});
$events->setParam('teamId', $team->getId());
@ -309,7 +313,9 @@ App::post('/v1/teams/:teamId/memberships')
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.')
->param('email', '', new Email(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
@ -319,9 +325,13 @@ App::post('/v1/teams/:teamId/memberships')
->inject('dbForProject')
->inject('locale')
->inject('mails')
->inject('messaging')
->inject('events')
->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Event $events) {
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) {
if (empty($userId) && empty($email) && empty($phone)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required');
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
@ -336,8 +346,31 @@ App::post('/v1/teams/:teamId/memberships')
if ($team->isEmpty()) {
throw new Exception(Exception::TEAM_NOT_FOUND);
}
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address
if (!empty($userId)) {
$invitee = $dbForProject->getDocument('users', $userId);
if ($invitee->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND, 'User with given userId doesn\'t exist.', 404);
}
if (!empty($email) && $invitee->getAttribute('email', '') != $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and email doesn\'t match', 409);
}
if (!empty($phone) && $invitee->getAttribute('phone', '') != $phone) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given userId and phone doesn\'t match', 409);
}
$email = $invitee->getAttribute('email', '');
$phone = $invitee->getAttribute('phone', '');
$name = empty($name) ? $invitee->getAttribute('name', '') : $name;
} elseif (!empty($email)) {
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address
if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') != $phone) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409);
}
} elseif (!empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') != $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
}
}
if (empty($invitee)) { // Create new user if no user with same email found
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@ -360,7 +393,8 @@ App::post('/v1/teams/:teamId/memberships')
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'email' => empty($email) ? null : $email,
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
@ -432,46 +466,50 @@ App::post('/v1/teams/:teamId/memberships')
} catch (Duplicate $th) {
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
}
}
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]);
$url = Template::unParseURL($url);
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]);
$url = Template::unParseURL($url);
if (!empty($email)) {
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
$body->setParam('{{owner}}', $user->getAttribute('name'));
$body->setParam('{{team}}', $team->getAttribute('name'));
$from = $project->isEmpty() || $project->getId() === 'console' ? '' : \sprintf($locale->getText('emails.sender'), $projectName);
$body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base.tpl');
$subject = \sprintf($locale->getText("emails.invitation.subject"), $team->getAttribute('name'), $projectName);
$body->setParam('{{owner}}', $user->getAttribute('name'));
$body->setParam('{{team}}', $team->getAttribute('name'));
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
->setParam('{{name}}', $user->getAttribute('name'))
->setParam('{{body}}', $locale->getText("emails.invitation.body"))
->setParam('{{redirect}}', $url)
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
->setParam('{{signature}}', $locale->getText("emails.invitation.signature"))
->setParam('{{project}}', $projectName)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000');
$body
->setParam('{{subject}}', $subject)
->setParam('{{hello}}', $locale->getText("emails.invitation.hello"))
->setParam('{{name}}', $user->getAttribute('name'))
->setParam('{{body}}', $locale->getText("emails.invitation.body"))
->setParam('{{redirect}}', $url)
->setParam('{{footer}}', $locale->getText("emails.invitation.footer"))
->setParam('{{thanks}}', $locale->getText("emails.invitation.thanks"))
->setParam('{{signature}}', $locale->getText("emails.invitation.signature"))
->setParam('{{project}}', $projectName)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{bg-body}}', '#f7f7f7')
->setParam('{{bg-content}}', '#ffffff')
->setParam('{{text-content}}', '#000000');
$body = $body->render();
$body = $body->render();
$mails
->setSubject($subject)
->setBody($body)
->setFrom($from)
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name'))
->trigger()
;
$mails
->setSubject($subject)
->setBody($body)
->setFrom($from)
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name'))
->trigger()
;
} elseif (!empty($phone)) {
$messaging
->setRecipient($phone)
->setMessage($url)
->trigger();
}
}
$events

View file

@ -34,11 +34,14 @@ use Utopia\Validator\Text;
use Utopia\Validator\Boolean;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PasswordDictionary;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Event $events): Document
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $events): Document
{
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
if (!empty($email)) {
$email = \strtolower($email);
@ -49,6 +52,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
? ID::unique()
: ID::custom($userId);
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
@ -61,10 +65,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'phone' => $phone,
'phoneVerification' => false,
'status' => true,
'password' => (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null,
'password' => $password,
'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@ -101,13 +106,15 @@ App::post('/v1/users')
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', null, new Password(), 'Plain text user password. Must be at least 8 chars.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $events);
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -134,10 +141,11 @@ App::post('/v1/users/bcrypt')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -164,10 +172,11 @@ App::post('/v1/users/md5')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -194,10 +203,11 @@ App::post('/v1/users/argon2')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -225,16 +235,17 @@ App::post('/v1/users/sha')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$options = '{}';
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $events);
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -261,10 +272,11 @@ App::post('/v1/users/phpass')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $events);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -296,9 +308,10 @@ App::post('/v1/users/scrypt')
->param('passwordLength', 64, new Integer(), 'Optional hash length used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Event $events) {
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
@ -307,7 +320,7 @@ App::post('/v1/users/scrypt')
'length' => $passwordLength
];
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $events);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -337,10 +350,11 @@ App::post('/v1/users/scrypt-modified')
->param('passwordSignerKey', '', new Text(128), 'Signer key used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $events);
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $events);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@ -779,11 +793,12 @@ App::patch('/v1/users/:userId/password')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User ID.')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $password, Response $response, Database $dbForProject, Event $events) {
->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -791,11 +806,27 @@ App::patch('/v1/users/:userId/password')
throw new Exception(Exception::USER_NOT_FOUND);
}
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = [];
if ($historyLimit > 0) {
$history = $user->getAttribute('passwordHistory', []);
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
if (!$validator->isValid($password)) {
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED, 'The password was recently used', 409);
}
$history[] = $newPassword;
array_slice($history, (count($history) - $historyLimit), $historyLimit);
}
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now());
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);

View file

@ -233,7 +233,7 @@ App::init()
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true')
@ -384,7 +384,7 @@ App::options()
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
@ -485,6 +485,10 @@ App::error()
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
break;
}
} elseif ($error instanceof Utopia\Database\Exception\Conflict) {
$error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, null, null, $error);
$code = $error->getCode();
$message = $error->getMessage();
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */

View file

@ -135,6 +135,8 @@ App::post('/v1/mock/tests/bar')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MOCK)
->label('sdk.offline.model', '/mock/tests/bar')
->label('sdk.offline.key', '{required}')
->label('sdk.mock', true)
->param('required', '', new Text(100), 'Sample string param')
->param('default', '', new Integer(true), 'Sample numeric param')

View file

@ -5,7 +5,6 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Usage\Stats;
@ -16,7 +15,6 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;

View file

@ -17,7 +17,7 @@ App::get('/versions')
];
foreach ($platforms as $platform) {
$languages = $platform['languages'] ?? [];
$languages = $platform['sdks'] ?? [];
foreach ($languages as $key => $language) {
if (isset($language['dev']) && $language['dev']) {

View file

@ -86,6 +86,7 @@ const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
const APP_LIMIT_USER_SESSIONS_MAX = 100;
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
const APP_LIMIT_ANTIVIRUS = 20000000; //20MB
@ -607,6 +608,12 @@ $register->set('smtp', function () {
$register->set('geodb', function () {
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2023-01.mmdb');
});
$register->set('passwordsDictionary', function () {
$content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords');
$content = explode("\n", $content);
$content = array_flip($content);
return $content;
});
$register->set('db', function () {
// This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
@ -1027,6 +1034,11 @@ App::setResource('geodb', function ($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('passwordsDictionary', function ($register) {
/** @var Utopia\Registry\Registry $register */
return $register->get('passwordsDictionary');
}, ['register']);
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
@ -1049,7 +1061,7 @@ App::setResource('servers', function () {
$languages = array_map(function ($language) {
return strtolower($language['name']);
}, $server['languages']);
}, $server['sdks']);
return $languages;
});
@ -1143,3 +1155,17 @@ App::setResource('schema', function ($utopia, $dbForProject) {
$params,
);
}, ['utopia', 'dbForProject']);
App::setResource('requestTimestamp', function ($request) {
// Validate x-appwrite-timestamp header
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
if (!empty($timestampHeader)) {
try {
$requestTimestamp = new \DateTime($timestampHeader);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value');
}
}
return $requestTimestamp;
}, ['request']);

View file

@ -26,7 +26,8 @@ $cli
->task('sdks')
->action(function () {
$platforms = Config::getParam('platforms');
$selected = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$selectedPlatform = Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
$version = Console::confirm('Choose an Appwrite version');
$git = (Console::confirm('Should we use git push? (yes/no)') == 'yes');
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
@ -37,8 +38,12 @@ $cli
}
foreach ($platforms as $key => $platform) {
foreach ($platform['languages'] as $language) {
if ($selected !== $language['key'] && $selected !== '*') {
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
continue;
}
foreach ($platform['sdks'] as $language) {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*') {
continue;
}
@ -81,8 +86,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
switch ($language['key']) {
case 'web':
$config = new Web();
$config->setNPMPackage('appwrite');
$config->setBowerPackage('appwrite');
if ($platform['key'] === APP_PLATFORM_CONSOLE) {
$config->setNPMPackage('@appwrite.io/console');
$config->setBowerPackage('@appwrite.io/console');
} else {
$config->setNPMPackage('appwrite');
$config->setBowerPackage('appwrite');
}
break;
case 'cli':
$config = new CLI();

View file

@ -43,13 +43,13 @@
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.18.*",
"utopia-php/abuse": "0.19.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.20.*",
"utopia-php/audit": "0.21.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.30.*",
"utopia-php/database": "0.31.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/domains": "1.1.*",
"utopia-php/framework": "0.26.*",

74
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": "ac80cafdd8c2c6deaec3dfe628084655",
"content-hash": "3e00aa37bea907b7dcca7f912402a392",
"packages": [
{
"name": "adhocore/jwt",
@ -1808,29 +1808,28 @@
},
{
"name": "utopia-php/abuse",
"version": "0.18.0",
"version": "0.19.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "8496401234f73a49f8c4259d3e89ab4a7c1f9ecf"
"reference": "419b6e2e0a5dec35ea83a25758df9cd129b6c412"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/8496401234f73a49f8c4259d3e89ab4a7c1f9ecf",
"reference": "8496401234f73a49f8c4259d3e89ab4a7c1f9ecf",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/419b6e2e0a5dec35ea83a25758df9cd129b6c412",
"reference": "419b6e2e0a5dec35ea83a25758df9cd129b6c412",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.30.*"
"utopia-php/database": "0.31.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
"phpstan/phpstan": "1.9.x-dev",
"phpunit/phpunit": "^9.4",
"vimeo/psalm": "4.0.1"
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.4"
},
"type": "library",
"autoload": {
@ -1852,9 +1851,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.18.0"
"source": "https://github.com/utopia-php/abuse/tree/0.19.0"
},
"time": "2023-02-14T09:56:04+00:00"
"time": "2023-02-26T03:28:48+00:00"
},
{
"name": "utopia-php/analytics",
@ -1913,22 +1912,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.20.0",
"version": "0.21.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "3fce3f4ad3ea9dfcb39b79668abd76331412a5ed"
"reference": "7f9783a14718c82570c6effb35a3cb42c30b13a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/3fce3f4ad3ea9dfcb39b79668abd76331412a5ed",
"reference": "3fce3f4ad3ea9dfcb39b79668abd76331412a5ed",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/7f9783a14718c82570c6effb35a3cb42c30b13a2",
"reference": "7f9783a14718c82570c6effb35a3cb42c30b13a2",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.30.*"
"utopia-php/database": "0.31.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -1956,9 +1955,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.20.0"
"source": "https://github.com/utopia-php/audit/tree/0.21.0"
},
"time": "2023-02-14T09:46:54+00:00"
"time": "2023-02-26T03:28:30+00:00"
},
{
"name": "utopia-php/cache",
@ -2115,16 +2114,16 @@
},
{
"name": "utopia-php/database",
"version": "0.30.1",
"version": "0.31.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "1cea72c1217357bf0747ae4f28ebef57e9dc0e65"
"reference": "61f9f4743a317f1d78558a5f981adf74e7fdc931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/1cea72c1217357bf0747ae4f28ebef57e9dc0e65",
"reference": "1cea72c1217357bf0747ae4f28ebef57e9dc0e65",
"url": "https://api.github.com/repos/utopia-php/database/zipball/61f9f4743a317f1d78558a5f981adf74e7fdc931",
"reference": "61f9f4743a317f1d78558a5f981adf74e7fdc931",
"shasum": ""
},
"require": {
@ -2163,9 +2162,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.30.1"
"source": "https://github.com/utopia-php/database/tree/0.31.0"
},
"time": "2023-02-14T06:25:03+00:00"
"time": "2023-02-23T09:49:44+00:00"
},
{
"name": "utopia-php/domains",
@ -3742,23 +3741,23 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.24",
"version": "9.2.25",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2cf940ebc6355a9d430462811b5aaa308b174bed"
"reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed",
"reference": "2cf940ebc6355a9d430462811b5aaa308b174bed",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0e2b40518197a8c0d4b08bc34dfff1c99c508954",
"reference": "0e2b40518197a8c0d4b08bc34dfff1c99c508954",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.14",
"nikic/php-parser": "^4.15",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -3807,7 +3806,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.25"
},
"funding": [
{
@ -3815,7 +3814,7 @@
"type": "github"
}
],
"time": "2023-01-26T08:26:55+00:00"
"time": "2023-02-25T05:32:00+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -5127,16 +5126,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.1",
"version": "3.7.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
"require": {
@ -5172,14 +5171,15 @@
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-18T07:21:10+00:00"
"time": "2023-02-22T23:07:41+00:00"
},
{
"name": "swoole/ide-helper",

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Account } from "appwrite";
import { Client, Account } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Avatars } from "appwrite";
import { Client, Avatars } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Databases } from "appwrite";
import { Client, Databases } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Functions } from "appwrite";
import { Client, Functions } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Functions } from "appwrite";
import { Client, Functions } from "@appwrite.io/console";
const client = new Client();

View file

@ -1,4 +1,4 @@
import { Client, Functions } from "appwrite";
import { Client, Functions } from "@appwrite.io/console";
const client = new Client();

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