Merge branch 'feat-db-pools' of github.com:appwrite/appwrite into refactor-internalId-collections-db-pools
This commit is contained in:
commit
213c4392a1
7 changed files with 213 additions and 1 deletions
3
.env
3
.env
|
@ -3,6 +3,7 @@ _APP_LOCALE=en
|
||||||
_APP_WORKER_PER_CORE=2
|
_APP_WORKER_PER_CORE=2
|
||||||
_APP_CONSOLE_WHITELIST_ROOT=disabled
|
_APP_CONSOLE_WHITELIST_ROOT=disabled
|
||||||
_APP_CONSOLE_WHITELIST_EMAILS=
|
_APP_CONSOLE_WHITELIST_EMAILS=
|
||||||
|
_APP_CONSOLE_WHITELIST_CODES=code-zero,code-one
|
||||||
_APP_CONSOLE_WHITELIST_IPS=
|
_APP_CONSOLE_WHITELIST_IPS=
|
||||||
_APP_CONSOLE_INVITES=enabled
|
_APP_CONSOLE_INVITES=enabled
|
||||||
_APP_SYSTEM_EMAIL_NAME=Appwrite
|
_APP_SYSTEM_EMAIL_NAME=Appwrite
|
||||||
|
@ -72,4 +73,4 @@ _APP_LOGGING_PROVIDER=
|
||||||
_APP_LOGGING_CONFIG=
|
_APP_LOGGING_CONFIG=
|
||||||
_APP_REGION=default
|
_APP_REGION=default
|
||||||
_APP_DOCKER_HUB_USERNAME=
|
_APP_DOCKER_HUB_USERNAME=
|
||||||
_APP_DOCKER_HUB_PASSWORD=
|
_APP_DOCKER_HUB_PASSWORD=
|
|
@ -125,6 +125,11 @@ return [
|
||||||
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
|
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
|
||||||
'code' => 401,
|
'code' => 401,
|
||||||
],
|
],
|
||||||
|
Exception::USER_CODE_INVALID => [
|
||||||
|
'name' => Exception::USER_CODE_INVALID,
|
||||||
|
'description' => 'The specified code is not valid. Contact your administrator for more information.',
|
||||||
|
'code' => 401,
|
||||||
|
],
|
||||||
Exception::USER_IP_NOT_WHITELISTED => [
|
Exception::USER_IP_NOT_WHITELISTED => [
|
||||||
'name' => Exception::USER_IP_NOT_WHITELISTED,
|
'name' => Exception::USER_IP_NOT_WHITELISTED,
|
||||||
'description' => 'Console registration is restricted to specific IPs. Contact your administrator for more information.',
|
'description' => 'Console registration is restricted to specific IPs. Contact your administrator for more information.',
|
||||||
|
|
|
@ -44,6 +44,98 @@ use Utopia\Validator\WhiteList;
|
||||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||||
$oauthDefaultFailure = '/auth/oauth2/failure';
|
$oauthDefaultFailure = '/auth/oauth2/failure';
|
||||||
|
|
||||||
|
App::post('/v1/account/invite')
|
||||||
|
->desc('Create account using an invite code')
|
||||||
|
->groups(['api', 'account', 'auth'])
|
||||||
|
->label('event', 'users.[userId].create')
|
||||||
|
->label('scope', 'public')
|
||||||
|
->label('auth.type', 'emailPassword')
|
||||||
|
->label('audits.event', 'user.create')
|
||||||
|
->label('audits.resource', 'user/{response.$id}')
|
||||||
|
->label('audits.userId', '{response.$id}')
|
||||||
|
->label('usage.metric', 'users.{scope}.requests.create')
|
||||||
|
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||||
|
->label('sdk.namespace', 'account')
|
||||||
|
->label('sdk.method', 'createWithInviteCode')
|
||||||
|
->label('sdk.description', '/docs/references/account/create.md')
|
||||||
|
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||||
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
|
->label('sdk.response.model', Response::MODEL_ACCOUNT)
|
||||||
|
->label('abuse-limit', 10)
|
||||||
|
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||||
|
->param('email', '', new Email(), 'User email.')
|
||||||
|
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
|
||||||
|
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||||
|
->param('code', '', new Text(128), 'An invite code to restrict user signups on the Appwrite console. Users with an invite code will be able to create accounts irrespective of email and IP whitelists.', true)
|
||||||
|
->inject('request')
|
||||||
|
->inject('response')
|
||||||
|
->inject('project')
|
||||||
|
->inject('dbForProject')
|
||||||
|
->inject('events')
|
||||||
|
->action(function (string $userId, string $email, string $password, string $name, string $code, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) {
|
||||||
|
|
||||||
|
if ($project->getId() !== 'console') {
|
||||||
|
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = \strtolower($email);
|
||||||
|
|
||||||
|
$whitelistCodes = (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null)) : [];
|
||||||
|
|
||||||
|
if (!empty($whitelistCodes) && !\in_array($code, $whitelistCodes)) {
|
||||||
|
throw new Exception(Exception::USER_CODE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
|
||||||
|
|
||||||
|
if ($limit !== 0) {
|
||||||
|
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
|
||||||
|
|
||||||
|
if ($total >= $limit) {
|
||||||
|
throw new Exception(Exception::USER_COUNT_EXCEEDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userId = $userId == 'unique()' ? ID::unique() : $userId;
|
||||||
|
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
|
||||||
|
'$id' => $userId,
|
||||||
|
'$permissions' => [
|
||||||
|
Permission::read(Role::any()),
|
||||||
|
Permission::update(Role::user($userId)),
|
||||||
|
Permission::delete(Role::user($userId)),
|
||||||
|
],
|
||||||
|
'email' => $email,
|
||||||
|
'emailVerification' => false,
|
||||||
|
'status' => true,
|
||||||
|
'password' => Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
|
||||||
|
'hash' => Auth::DEFAULT_ALGO,
|
||||||
|
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||||
|
'passwordUpdate' => DateTime::now(),
|
||||||
|
'registration' => DateTime::now(),
|
||||||
|
'reset' => false,
|
||||||
|
'name' => $name,
|
||||||
|
'prefs' => new \stdClass(),
|
||||||
|
'sessions' => null,
|
||||||
|
'tokens' => null,
|
||||||
|
'memberships' => null,
|
||||||
|
'search' => implode(' ', [$userId, $email, $name])
|
||||||
|
])));
|
||||||
|
} catch (Duplicate $th) {
|
||||||
|
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Authorization::unsetRole(Role::guests()->toString());
|
||||||
|
Authorization::setRole(Role::user($user->getId())->toString());
|
||||||
|
Authorization::setRole(Role::users()->toString());
|
||||||
|
|
||||||
|
$events->setParam('userId', $user->getId());
|
||||||
|
|
||||||
|
$response
|
||||||
|
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||||
|
->dynamic($user, Response::MODEL_ACCOUNT);
|
||||||
|
});
|
||||||
|
|
||||||
App::post('/v1/account')
|
App::post('/v1/account')
|
||||||
->desc('Create Account')
|
->desc('Create Account')
|
||||||
->groups(['api', 'account', 'auth'])
|
->groups(['api', 'account', 'auth'])
|
||||||
|
|
|
@ -103,6 +103,7 @@ services:
|
||||||
- _APP_LOCALE
|
- _APP_LOCALE
|
||||||
- _APP_CONSOLE_WHITELIST_ROOT
|
- _APP_CONSOLE_WHITELIST_ROOT
|
||||||
- _APP_CONSOLE_WHITELIST_EMAILS
|
- _APP_CONSOLE_WHITELIST_EMAILS
|
||||||
|
- _APP_CONSOLE_WHITELIST_CODES
|
||||||
- _APP_CONSOLE_WHITELIST_IPS
|
- _APP_CONSOLE_WHITELIST_IPS
|
||||||
- _APP_CONSOLE_INVITES
|
- _APP_CONSOLE_INVITES
|
||||||
- _APP_SYSTEM_EMAIL_NAME
|
- _APP_SYSTEM_EMAIL_NAME
|
||||||
|
|
|
@ -60,6 +60,7 @@ class Exception extends \Exception
|
||||||
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
|
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
|
||||||
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
|
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted';
|
||||||
public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
|
public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted';
|
||||||
|
public const USER_CODE_INVALID = 'user_code_invalid';
|
||||||
public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
|
public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials';
|
||||||
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
|
public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
|
||||||
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
||||||
|
|
|
@ -2,13 +2,98 @@
|
||||||
|
|
||||||
namespace Tests\E2E\Services\Account;
|
namespace Tests\E2E\Services\Account;
|
||||||
|
|
||||||
|
use Appwrite\Extend\Exception;
|
||||||
use Tests\E2E\Scopes\Scope;
|
use Tests\E2E\Scopes\Scope;
|
||||||
use Tests\E2E\Scopes\ProjectConsole;
|
use Tests\E2E\Scopes\ProjectConsole;
|
||||||
use Tests\E2E\Scopes\SideClient;
|
use Tests\E2E\Scopes\SideClient;
|
||||||
|
use Utopia\Database\ID;
|
||||||
|
use Utopia\Database\DateTime;
|
||||||
|
use Tests\E2E\Client;
|
||||||
|
|
||||||
class AccountConsoleClientTest extends Scope
|
class AccountConsoleClientTest extends Scope
|
||||||
{
|
{
|
||||||
use AccountBase;
|
use AccountBase;
|
||||||
use ProjectConsole;
|
use ProjectConsole;
|
||||||
use SideClient;
|
use SideClient;
|
||||||
|
|
||||||
|
public function testCreateAccountWithInvite(): void
|
||||||
|
{
|
||||||
|
$email = uniqid() . 'user@localhost.test';
|
||||||
|
$password = 'password';
|
||||||
|
$name = 'User Name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for FAILURE
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
'code' => 'Invalid Code'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($response['headers']['status-code'], 401);
|
||||||
|
$this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($response['headers']['status-code'], 401);
|
||||||
|
$this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for SUCCESS
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
'code' => 'code-zero'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
$this->assertNotEmpty($response['body']);
|
||||||
|
$this->assertNotEmpty($response['body']['$id']);
|
||||||
|
$this->assertEquals(true, DateTime::isValid($response['body']['registration']));
|
||||||
|
$this->assertEquals($response['body']['email'], $email);
|
||||||
|
$this->assertEquals($response['body']['name'], $name);
|
||||||
|
|
||||||
|
$email = uniqid() . 'user@localhost.test';
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
'code' => 'code-one'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
$this->assertNotEmpty($response['body']);
|
||||||
|
$this->assertNotEmpty($response['body']['$id']);
|
||||||
|
$this->assertEquals(true, DateTime::isValid($response['body']['registration']));
|
||||||
|
$this->assertEquals($response['body']['email'], $email);
|
||||||
|
$this->assertEquals($response['body']['name'], $name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Tests\E2E\Services\Account;
|
namespace Tests\E2E\Services\Account;
|
||||||
|
|
||||||
|
use Appwrite\Extend\Exception;
|
||||||
use Appwrite\SMS\Adapter\Mock;
|
use Appwrite\SMS\Adapter\Mock;
|
||||||
use Tests\E2E\Client;
|
use Tests\E2E\Client;
|
||||||
use Tests\E2E\Scopes\Scope;
|
use Tests\E2E\Scopes\Scope;
|
||||||
|
@ -18,6 +19,32 @@ class AccountCustomClientTest extends Scope
|
||||||
use ProjectCustom;
|
use ProjectCustom;
|
||||||
use SideClient;
|
use SideClient;
|
||||||
|
|
||||||
|
public function testCreateAccountWithInvite(): void
|
||||||
|
{
|
||||||
|
$email = uniqid() . 'user@localhost.test';
|
||||||
|
$password = 'password';
|
||||||
|
$name = 'User Name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for FAILURE
|
||||||
|
* Make sure the invite endpoint is only accessible through the console project.
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
'code' => 'Invalid Code'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($response['headers']['status-code'], 401);
|
||||||
|
$this->assertEquals($response['body']['type'], Exception::GENERAL_ACCESS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @depends testCreateAccountSession
|
* @depends testCreateAccountSession
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue