1
0
Fork 0
mirror of synced 2024-06-27 02:31:04 +12:00

Merge branch 'feat-db-pools' of github.com:appwrite/appwrite into feat-new-usage-stats

This commit is contained in:
shimon 2023-01-15 17:18:32 +02:00
commit a852672bff
7 changed files with 213 additions and 1 deletions

3
.env
View file

@ -3,6 +3,7 @@ _APP_LOCALE=en
_APP_WORKER_PER_CORE=2
_APP_CONSOLE_WHITELIST_ROOT=disabled
_APP_CONSOLE_WHITELIST_EMAILS=
_APP_CONSOLE_WHITELIST_CODES=code-zero,code-one
_APP_CONSOLE_WHITELIST_IPS=
_APP_CONSOLE_INVITES=enabled
_APP_SYSTEM_EMAIL_NAME=Appwrite
@ -68,4 +69,4 @@ _APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=
_APP_REGION=default
_APP_DOCKER_HUB_USERNAME=
_APP_DOCKER_HUB_PASSWORD=
_APP_DOCKER_HUB_PASSWORD=

View file

@ -125,6 +125,11 @@ return [
'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.',
'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 => [
'name' => Exception::USER_IP_NOT_WHITELISTED,
'description' => 'Console registration is restricted to specific IPs. Contact your administrator for more information.',

View file

@ -44,6 +44,98 @@ use Utopia\Validator\WhiteList;
$oauthDefaultSuccess = '/auth/oauth2/success';
$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')
->desc('Create Account')
->groups(['api', 'account', 'auth'])

View file

@ -111,6 +111,7 @@ services:
- _APP_LOCALE
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_CODES
- _APP_CONSOLE_WHITELIST_IPS
- _APP_CONSOLE_INVITES
- _APP_SYSTEM_EMAIL_NAME

View file

@ -60,6 +60,7 @@ class Exception extends \Exception
public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required';
public const USER_EMAIL_NOT_WHITELISTED = 'user_email_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_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';

View file

@ -2,13 +2,98 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
use Utopia\Database\DateTime;
use Tests\E2E\Client;
class AccountConsoleClientTest extends Scope
{
use AccountBase;
use ProjectConsole;
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);
}
}

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Appwrite\SMS\Adapter\Mock;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
@ -18,6 +19,32 @@ class AccountCustomClientTest extends Scope
use ProjectCustom;
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
*/