1
0
Fork 0
mirror of synced 2024-06-02 19:04:49 +12:00

Merge branch '0.8.x' of github.com:appwrite/appwrite into feat-add-update-membership-endpoint

This commit is contained in:
Eldad Fux 2021-05-13 20:49:52 +03:00
commit 3704dc69e8
24 changed files with 282 additions and 59 deletions

3
.env
View file

@ -1,6 +1,9 @@
_APP_ENV=production
_APP_ENV=development
_APP_LOCALE=en
_APP_CONSOLE_WHITELIST_ROOT=disabled
_APP_CONSOLE_WHITELIST_EMAILS=
_APP_CONSOLE_WHITELIST_IPS=
_APP_SYSTEM_EMAIL_NAME=Appwrite
_APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io

View file

@ -1,24 +1,24 @@
# Version 0.8.0 (Not Released Yet)
## Features
- Refactoring SSL generation to work on every request so no domain environment variable is required for SSL generation (#1133)
- Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md), #914)
- Added events for functions and executions (#971)
- Added JWT support (#784)
- Added ARM support (#726)
- Splitted token & session models to become 2 different internal entities (#922)
- Split token & session models to become 2 different internal entities (#922)
- Added Dart 2.12 as a new Cloud Functions runtime (#989)
- Added option to disable email/password (#947)
- Added option to disable anonymous login (need to merge and apply changed) (#947)
- Added option to disable JWT auth (#947)
- Added option to disable team invites (#947)
- Option to limit number of users (good for app launches + god account PR) (#947)
- Option to limit number of users (good for app launches + root account PR) (#947)
- Added 2 new endpoints to the projects API to allow new settings
- Enabled 501 errors (Not Implemented) from the error handler
- Added Python 3.9 as a new Cloud Functions runtime (#1044)
- Added Deno 1.8 as a new Cloud Functions runtime (#989)
- Upgraded to PHP 8.0 (#713)
- ClamAV is now disabled by default to allow lower min requirments for Appwrite (#1064)
- ClamAV is now disabled by default to allow lower min requirements for Appwrite (#1064)
- Added a new env var named `_APP_LOCALE` that allow to change the default `en` locale value (#1056)
- Updated all the console bottom control to be consistent. Dropped the `+` icon (#1062)
- Added Response Models for Documents and Preferences (#1075, #1102)
@ -29,6 +29,9 @@
- Fixed default value for HTTPS force option
- Fixed form array casting in dashboard (#1070)
- Fixed collection document rule form in dashboard (#1069)
- Bugs in the Teams API:
- Fixed incorrect audit worker event names (#1143)
- Increased limit of memberships fetched in `createTeamMembership` to 2000 (#1143)
## Breaking Changes (Read before upgrading!)

View file

@ -88,6 +88,13 @@ ENV _APP_SERVER=swoole \
_APP_DOMAIN_TARGET=localhost \
_APP_HOME=https://appwrite.io \
_APP_EDITION=community \
_APP_CONSOLE_WHITELIST_ROOT=enabled \
_APP_CONSOLE_WHITELIST_EMAILS= \
_APP_CONSOLE_WHITELIST_IPS= \
_APP_SYSTEM_EMAIL_NAME= \
_APP_SYSTEM_EMAIL_ADDRESS= \
_APP_SYSTEM_RESPONSE_FORMAT= \
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS= \
_APP_OPTIONS_ABUSE=enabled \
_APP_OPTIONS_FORCE_HTTPS=disabled \
_APP_OPENSSL_KEY_V1=your-secret-key \

View file

@ -46,7 +46,7 @@ $collections = [
'legalTaxId' => '',
'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
'authWhitelistDomains' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [],
'usersAuthLimit' => (App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
],
Database::SYSTEM_COLLECTION_COLLECTIONS => [
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,

View file

@ -63,9 +63,17 @@ return [
'required' => true,
'question' => 'Enter a DNS A record hostname to serve as a CNAME for your custom domains.\nYou can use the same value as used for the Appwrite hostname.',
],
[
'name' => '_APP_CONSOLE_WHITELIST_ROOT',
'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by invting them to your project. By default this option is enabled.',
'introduction' => '0.8.0',
'default' => 'enabled',
'required' => false,
'question' => '',
],
[
'name' => '_APP_CONSOLE_WHITELIST_EMAILS',
'description' => 'This option allows you to limit creation of users to Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma.',
'description' => 'This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma.',
'introduction' => '',
'default' => '',
'required' => false,

View file

@ -61,7 +61,6 @@ App::post('/v1/account')
if ('console' === $project->getId()) {
$whitlistEmails = $project->getAttribute('authWhitelistEmails');
$whitlistIPs = $project->getAttribute('authWhitelistIPs');
$whitlistDomains = $project->getAttribute('authWhitelistDomains');
if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) {
throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401);
@ -70,10 +69,6 @@ App::post('/v1/account')
if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) {
throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401);
}
if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) {
throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401);
}
}
$limit = $project->getAttribute('usersAuthLimit', 0);
@ -514,7 +509,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'emailVerification' => true,
'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'passwordUpdate' => \time(),
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
'name' => $name,
@ -1012,7 +1007,7 @@ App::patch('/v1/account/password')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('password', '', new Password(), 'New user password. Must be between 6 to 32 chars.')
->param('oldPassword', '', new Password(), 'Old user password. Must be between 6 to 32 chars.')
->param('oldPassword', '', new Password(), 'Old user password. Must be between 6 to 32 chars.', true)
->inject('response')
->inject('user')
->inject('projectDB')
@ -1023,12 +1018,14 @@ App::patch('/v1/account/password')
/** @var Appwrite\Database\Database $projectDB */
/** @var Appwrite\Event\Event $audits */
if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
throw new Exception('Invalid credentials', 401);
}
$user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [
'password' => Auth::passwordHash($password),
'passwordUpdate' => \time(),
]));
if (false === $user) {

View file

@ -272,7 +272,7 @@ App::get('/v1/health/anti-virus')
App::get('/v1/health/stats') // Currently only used internally
->desc('Get System Stats')
->groups(['api', 'health'])
->label('scope', 'god')
->label('scope', 'root')
// ->label('sdk.auth', [APP_AUTH_TYPE_KEY])
// ->label('sdk.namespace', 'health')
// ->label('sdk.method', 'getStats')

View file

@ -612,7 +612,7 @@ App::delete('/v1/storage/files/:fileId')
// App::get('/v1/storage/files/:fileId/scan')
// ->desc('Scan Storage')
// ->groups(['api', 'storage'])
// ->label('scope', 'god')
// ->label('scope', 'root')
// ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
// ->label('sdk.namespace', 'storage')
// ->label('sdk.method', 'getFileScan')

View file

@ -294,7 +294,7 @@ App::post('/v1/teams/:teamId/memberships')
}
$memberships = $projectDB->getCollection([
'limit' => 50,
'limit' => 2000,
'offset' => 0,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS,
@ -341,7 +341,12 @@ App::post('/v1/teams/:teamId/memberships')
'emailVerification' => false,
'status' => Auth::USER_STATUS_UNACTIVATED,
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'passwordUpdate' => \time(),
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
* old password
*/
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
'name' => $name,
@ -446,7 +451,7 @@ App::post('/v1/teams/:teamId/memberships')
if (!$isPrivilegedUser && !$isAppUser) { // No need of confirmation when in admin or app mode
$mails
->setParam('event', 'teams.membership.create')
->setParam('event', 'teams.memberships.create')
->setParam('from', ($project->getId() === 'console') ? '' : \sprintf($locale->getText('account.emails.team'), $project->getAttribute('name')))
->setParam('recipient', $email)
->setParam('name', $name)
@ -458,7 +463,7 @@ App::post('/v1/teams/:teamId/memberships')
$audits
->setParam('userId', $invitee->getId())
->setParam('event', 'teams.membership.create')
->setParam('event', 'teams.memberships.create')
->setParam('resource', 'teams/'.$teamId)
;
@ -643,7 +648,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
}
if ($userId != $membership->getAttribute('userId')) {
throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401);
throw new Exception('Invite does not belong to current user ('.$user->getAttribute('email').')', 401);
}
if (empty($user->getId())) {
@ -657,7 +662,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
}
if ($membership->getAttribute('userId') !== $user->getId()) {
throw new Exception('Invite not belong to current user ('.$user->getAttribute('email').')', 401);
throw new Exception('Invite does not belong to current user ('.$user->getAttribute('email').')', 401);
}
$membership // Attach user to team
@ -713,7 +718,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$audits
->setParam('userId', $user->getId())
->setParam('event', 'teams.membership.update')
->setParam('event', 'teams.memberships.update.status')
->setParam('resource', 'teams/'.$teamId)
;
@ -789,7 +794,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
$audits
->setParam('userId', $membership->getAttribute('userId'))
->setParam('event', 'teams.membership.delete')
->setParam('event', 'teams.memberships.delete')
->setParam('resource', 'teams/'.$teamId)
;

View file

@ -14,8 +14,6 @@ use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Network\Validator\Origin;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Appwrite\Utopia\Response\Filters\V06;
use Utopia\CLI\Console;
@ -23,15 +21,61 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $clients) {
App::init(function ($utopia, $request, $response, $console, $project, $consoleDB, $user, $locale, $clients) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $consoleDB */
/** @var Appwrite\Database\Document $console */
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
/** @var bool $mode */
/** @var array $clients */
$domain = $request->getHostname();
$domains = Config::getParam('domains', []);
if (!array_key_exists($domain, $domains)) {
$domain = new Domain(!empty($domain) ? $domain : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$domains[$domain->get()] = false;
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
} else {
Authorization::disable();
$dbDomain = $consoleDB->getCollectionFirst([
'limit' => 1,
'offset' => 0,
'filters' => [
'$collection=' . Database::SYSTEM_COLLECTION_CERTIFICATES,
'domain=' . $domain->get(),
],
]);
if (empty($dbDomain)) {
$dbDomain = [
'$collection' => Database::SYSTEM_COLLECTION_CERTIFICATES,
'$permissions' => [
'read' => [],
'write' => [],
],
'domain' => $domain->get(),
];
$dbDomain = $consoleDB->createDocument($dbDomain);
Authorization::enable();
Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in ~30 seconds..'); // TODO move this to installation script
ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [
'document' => $dbDomain,
'domain' => $domain->get(),
'validateTarget' => false,
'validateCNAME' => false,
]);
}
$domains[$domain->get()] = true;
}
Config::setParam('domains', $domains);
}
$localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@ -208,7 +252,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
}
}, $user->getAttribute('memberships', []));
// TDOO Check if user is god
// TDOO Check if user is root
if (!\in_array($scope, $scopes)) {
if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing
@ -226,7 +270,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo
throw new Exception('Password reset is required', 412);
}
}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'clients']);
}, ['utopia', 'request', 'response', 'console', 'project', 'consoleDB', 'user', 'locale', 'clients']);
App::options(function ($request, $response) {
/** @var Utopia\Swoole\Request $request */
@ -424,4 +468,4 @@ include_once __DIR__ . '/shared/web.php';
foreach (Config::getParam('services', []) as $service) {
include_once $service['controller'];
}
}

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Database\Database;
use Appwrite\Specification\Format\OpenAPI3;
use Appwrite\Specification\Format\Swagger2;
use Appwrite\Specification\Specification;
@ -42,10 +43,38 @@ App::get('/')
->label('permission', 'public')
->label('scope', 'home')
->inject('response')
->action(function ($response) {
->inject('consoleDB')
->inject('project')
->action(function ($response, $consoleDB, $project) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $consoleDB */
/** @var Appwrite\Database\Document $project */
$response->redirect('/auth/signin');
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Expires', 0)
->addHeader('Pragma', 'no-cache')
;
if ('console' === $project->getId() || $project->isEmpty()) {
$whitlistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled');
if($whitlistRoot !== 'disabled') {
$consoleDB->getCollection([ // Count users
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
],
]);
$sum = $consoleDB->getSum();
if($sum !== 0) {
return $response->redirect('/auth/signin');
}
}
}
$response->redirect('/auth/signup');
});
App::get('/auth/signin')
@ -58,6 +87,10 @@ App::get('/auth/signin')
$page = new View(__DIR__.'/../../views/home/auth/signin.phtml');
$page
->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled'))
;
$layout
->setParam('title', 'Sign In - '.APP_NAME)
->setParam('body', $page);
@ -72,6 +105,10 @@ App::get('/auth/signup')
/** @var Utopia\View $layout */
$page = new View(__DIR__.'/../../views/home/auth/signup.phtml');
$page
->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled'))
;
$layout
->setParam('title', 'Sign Up - '.APP_NAME)
->setParam('body', $page);
@ -87,6 +124,10 @@ App::get('/auth/recovery')
$page = new View(__DIR__.'/../../views/home/auth/recovery.phtml');
$page
->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST'))))
;
$layout
->setParam('title', 'Password Recovery - '.APP_NAME)
->setParam('body', $page);

View file

@ -12,6 +12,8 @@ use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
// xdebug_start_trace('/tmp/trace');
@ -65,18 +67,6 @@ Files::load(__DIR__ . '/../public');
include __DIR__ . '/controllers/general.php';
$domain = App::getEnv('_APP_DOMAIN', '');
Console::info('Issuing a TLS certificate for the master domain ('.$domain.') in 30 seconds.
Make sure your domain points to your server IP or restart your Appwrite server to try again.'); // TODO move this to installation script
ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [
'document' => [],
'domain' => $domain,
'validateTarget' => false,
'validateCNAME' => false,
]);
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) {
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);

View file

@ -61,12 +61,12 @@ $cli
Console::log('🟢 Abuse protection is enabled');
}
$authWhitelistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', null);
$authWhitelistEmails = App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null);
$authWhitelistIPs = App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null);
$authWhitelistDomains = App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null);
if(empty($authWhitelistEmails)
&& empty($authWhitelistDomains)
if(empty($authWhitelistRoot)
&& empty($authWhitelistEmails)
&& empty($authWhitelistIPs)
) {
Console::log('🔴 Console access limits are disabled');

View file

@ -1,6 +1,6 @@
<?php
$home = $this->getParam('home', '');
$version = $this->getParam('version', '').'.'.APP_CACHE_BUSTER;
$version = $this->getParam('version', '') . '.' . APP_CACHE_BUSTER;
?>
<footer class="clear margin-top-large">
<ul class="copyright pull-start">
@ -12,6 +12,14 @@ $version = $this->getParam('version', '').'.'.APP_CACHE_BUSTER;
data-analytics-label="GitHub Link"
href="https://github.com/appwrite/appwrite" target="_blank" rel="noopener"><i class="icon-github-circled"></i> GitHub</a>
</li>
<li>
<a class="link-animation-enabled"
data-analytics
data-analytics-event="click"
data-analytics-category="console/footer"
data-analytics-label="Discord Link"
href="https://appwrite.io/discord" target="_blank" rel="noopener"><i class="icon-discord"></i> Discord</a>
</li>
<li>
<a class="link-animation-enabled"
data-analytics

View file

@ -1,3 +1,6 @@
<?php
$smtpEnabled = $this->getParam('smtpEnabled', false);
?>
<div class="zone medium">
<h1 class="zone xl margin-bottom-large margin-top">
Password Recovery
@ -25,7 +28,13 @@
<input name="url" type="hidden" data-ls-bind="{{env.ENDPOINT}}/auth/recovery/reset" />
<button type="submit" class="btn btn-primary"><i class="fa fa-sign-in"></i> Recover</button>
<?php if(!$smtpEnabled): ?>
<div class="box note padding-tiny warning margin-bottom text-align-center">
<i class="icon-warning"></i> SMTP connection is disabled. <a href="https://appwrite.io/docs/email-delivery" target="_blank" rel="noopener">Learn more <i class="icon-link-ext"></i></a>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary"<?php if(!$smtpEnabled): ?> disabled<?php endif; ?>><i class="fa fa-sign-in"></i> Recover</button>
</form>
</div>

View file

@ -1,3 +1,6 @@
<?php
$root = ($this->getParam('root') !== 'disabled');
?>
<div class="zone medium"
data-service="account.get"
data-name="account"
@ -43,7 +46,7 @@
<br />
<div class="text-line-high-large text-align-center">
<a href="/auth/recovery">Forgot password?</a> or don't have an account? <b><a href="/auth/signup">Sign up now</a></b>
<a href="/auth/recovery">Forgot password?</a><?php if(!$root): ?> or don't have an account? <b><a href="/auth/signup">Sign up now</a></b><?php endif; ?>
</div>
</div>

View file

@ -1,3 +1,6 @@
<?php
$root = ($this->getParam('root') !== 'disabled');
?>
<div class="zone medium signup">
<h1 class="zone xl margin-bottom-large margin-top">
Sign Up
@ -23,6 +26,10 @@
data-failure-param-alert-text="Registration Failed. Please try again later"
data-failure-param-alert-classname="error">
<?php if($root): ?>
<p>Please create your root account</p>
<?php endif; ?>
<label>Name</label>
<input name="name" type="text" autocomplete="name" placeholder="" required maxlength="128">
@ -44,6 +51,8 @@
</div>
<?php if(!$root): ?>
<div class="zone medium text-align-center">
<a href="/auth/signin">Already have an account?</a>
</div>
</div>
<?PHP endif; ?>

View file

@ -57,6 +57,7 @@ services:
environment:
- _APP_ENV
- _APP_LOCALE
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_IPS
- _APP_SYSTEM_EMAIL_NAME

View file

@ -76,6 +76,9 @@ services:
environment:
- _APP_ENV
- _APP_LOCALE
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_WHITELIST_IPS
- _APP_SYSTEM_EMAIL_NAME
- _APP_SYSTEM_EMAIL_ADDRESS
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS

View file

@ -1 +1 @@
Update currently logged in user password. For validation, user is required to pass in the new password, and the old password.
Update currently logged in user password. For validation, user is required to pass in the new password, and the old password. For users created with OAuth and Team Invites, oldPassword is optional.

View file

@ -34,6 +34,12 @@ class User extends Model
'default' => 0,
'example' => 0,
])
->addRule('passwordUpdate', [
'type' => self::TYPE_INTEGER,
'description' => 'Unix timestamp of the most recent password update',
'default' => 0,
'example' => 1592981250,
])
->addRule('email', [
'type' => self::TYPE_STRING,
'description' => 'User email address.',

View file

@ -509,6 +509,33 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 400);
/**
* Existing user tries to update password by passing wrong old password -> SHOULD FAIL
*/
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'new-password',
'oldPassword' => $password,
]);
$this->assertEquals($response['headers']['status-code'], 401);
/**
* Existing user tries to update password without passing old password -> SHOULD FAIL
*/
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'new-password'
]);
$this->assertEquals($response['headers']['status-code'], 401);
$data['password'] = 'new-password';
return $data;

View file

@ -272,11 +272,10 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'new-password',
'oldPassword' => '',
]);
$this->assertEquals($response['headers']['status-code'], 400);
$this->assertEquals(400, $response['headers']['status-code']);
return $session;
}

View file

@ -43,6 +43,7 @@ trait TeamsBaseClient
$teamUid = $data['teamUid'] ?? '';
$teamName = $data['teamName'] ?? '';
$email = uniqid().'friend@localhost.test';
$name = 'Friend User';
/**
* Test for SUCCESS
@ -52,7 +53,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'name' => 'Friend User',
'name' => $name,
'roles' => ['admin', 'editor'],
'url' => 'http://localhost:5000/join-us#title'
]);
@ -68,7 +69,7 @@ trait TeamsBaseClient
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals('Friend User', $lastEmail['to'][0]['name']);
$this->assertEquals($name, $lastEmail['to'][0]['name']);
$this->assertEquals('Invitation to '.$teamName.' Team at '.$this->getProject()['name'], $lastEmail['subject']);
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
@ -83,7 +84,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => 'dasdkaskdjaskdjasjkd',
'name' => 'Friend User',
'name' => $name,
'roles' => ['admin', 'editor'],
'url' => 'http://localhost:5000/join-us#title'
]);
@ -95,7 +96,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'name' => 'Friend User',
'name' => $name,
'roles' => 'bad string',
'url' => 'http://localhost:5000/join-us#title'
]);
@ -107,7 +108,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'email' => $email,
'name' => 'Friend User',
'name' => $name,
'roles' => ['admin', 'editor'],
'url' => 'http://example.com/join-us#title' // bad url
]);
@ -119,6 +120,8 @@ trait TeamsBaseClient
'secret' => $secret,
'membershipUid' => $membershipUid,
'userUid' => $userUid,
'email' => $email,
'name' => $name
];
}
@ -131,6 +134,8 @@ trait TeamsBaseClient
$secret = $data['secret'] ?? '';
$membershipUid = $data['membershipUid'] ?? '';
$userUid = $data['userUid'] ?? '';
$email = $data['email'] ?? '';
$name = $data['name'] ?? '';
/**
* Test for SUCCESS
@ -151,6 +156,61 @@ trait TeamsBaseClient
$this->assertCount(2, $response['body']['roles']);
$this->assertIsInt($response['body']['joined']);
$this->assertEquals(true, $response['body']['confirm']);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']];
/**
* New User tries to update password without old password -> SHOULD PASS
*/
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'new-password'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertIsNumeric($response['body']['registration']);
$this->assertEquals($response['body']['email'], $email);
$this->assertEquals($response['body']['name'], $name);
/**
* New User again tries to update password with ONLY new password -> SHOULD FAIL
*/
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'new-password',
]);
$this->assertEquals(401, $response['headers']['status-code']);
/**
* New User tries to update password by passing both old and new password -> SHOULD PASS
*/
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session,
]), [
'password' => 'newer-password',
'oldPassword' => 'new-password'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertIsNumeric($response['body']['registration']);
$this->assertEquals($response['body']['email'], $email);
$this->assertEquals($response['body']['name'], $name);
var_dump($response);