1
0
Fork 0
mirror of synced 2024-06-03 03:14:50 +12:00
appwrite/app/controllers/api/account.php

2155 lines
92 KiB
PHP
Raw Normal View History

2019-05-09 18:54:39 +12:00
<?php
2021-06-13 07:37:19 +12:00
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
2021-02-15 06:28:54 +13:00
use Appwrite\Detector\Detector;
2021-05-07 10:31:05 +12:00
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Network\Validator\URL;
2021-08-05 17:06:38 +12:00
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Response;
2022-01-19 00:05:04 +13:00
use Appwrite\Utopia\Database\Validator\CustomId;
2021-05-07 10:31:05 +12:00
use Utopia\App;
use Utopia\Audit\Audit;
2021-08-05 17:06:38 +12:00
use Utopia\Config\Config;
2021-05-07 10:31:05 +12:00
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Appwrite\Extend\Exception;
2021-08-05 17:06:38 +12:00
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Range;
2021-08-05 17:06:38 +12:00
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
2019-05-09 18:54:39 +12:00
$oauthDefaultSuccess = '/v1/auth/oauth2/success';
$oauthDefaultFailure = '/v1/auth/oauth2/failure';
2020-04-09 01:38:36 +12:00
2020-06-29 05:31:21 +12:00
App::post('/v1/account')
2020-01-23 12:32:10 +13:00
->desc('Create Account')
2021-03-01 07:36:13 +13:00
->groups(['api', 'account', 'auth'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].create')
2020-01-06 00:29:42 +13:00
->label('scope', 'public')
2021-03-01 07:36:13 +13:00
->label('auth.type', 'emailPassword')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [])
2020-01-04 10:00:53 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'create')
2020-01-06 00:29:42 +13:00
->label('sdk.description', '/docs/references/account/create.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
2020-01-04 10:00:53 +13:00
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "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.')
2020-09-11 02:40:14 +12:00
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
2020-09-11 02:40:14 +12:00
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($userId, $email, $password, $name, $request, $response, $project, $dbForProject, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
$email = \strtolower($email);
2020-06-30 09:43:34 +12:00
if ('console' === $project->getId()) {
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
$whitelistIPs = $project->getAttribute('authWhitelistIPs');
2020-06-30 09:43:34 +12:00
if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails)) {
throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401, Exception::USER_EMAIL_NOT_WHITELISTED);
2020-01-04 10:00:53 +13:00
}
if (!empty($whitelistIPs) && !\in_array($request->getIP(), $whitelistIPs)) {
throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401, Exception::USER_IP_NOT_WHITELISTED);
2020-01-04 10:00:53 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2021-08-06 20:34:17 +12:00
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
2021-03-01 07:36:13 +13:00
if ($limit !== 0) {
2022-02-27 22:57:09 +13:00
$total = $dbForProject->count('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
], APP_LIMIT_USERS);
2021-03-01 07:36:13 +13:00
2022-02-27 22:57:09 +13:00
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
2021-03-01 07:36:13 +13:00
}
}
2020-06-30 09:43:34 +12:00
try {
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
2021-05-07 10:31:05 +12:00
'$id' => $userId,
2021-06-12 06:23:16 +12:00
'$read' => ['role:all'],
2021-08-05 17:06:38 +12:00
'$write' => ['user:' . $userId],
2020-06-30 09:43:34 +12:00
'email' => $email,
'emailVerification' => false,
'status' => true,
2020-06-30 09:43:34 +12:00
'password' => Auth::passwordHash($password),
'passwordUpdate' => \time(),
2020-06-30 09:43:34 +12:00
'registration' => \time(),
'reset' => false,
'name' => $name,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass(),
2022-04-26 22:36:49 +12:00
'sessions' => null,
2022-04-27 23:06:53 +12:00
'tokens' => null,
2022-04-28 00:44:47 +12:00
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
])));
2020-06-30 09:43:34 +12:00
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS);
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2021-08-05 17:06:38 +12:00
Authorization::unsetRole('role:' . Auth::USER_ROLE_GUEST);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole('role:' . Auth::USER_ROLE_MEMBER);
2020-11-21 10:02:26 +13:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2020-06-30 09:43:34 +12:00
;
2022-04-19 21:30:42 +12:00
$usage->setParam('users.create', 1);
$events->setParam('userId', $user->getId());
2022-04-04 18:30:07 +12:00
2021-05-07 10:31:05 +12:00
$response->setStatusCode(Response::STATUS_CODE_CREATED);
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2020-06-29 05:31:21 +12:00
App::post('/v1/account/sessions')
2020-01-06 00:29:42 +13:00
->desc('Create Account Session')
2021-03-01 10:22:03 +13:00
->groups(['api', 'account', 'auth'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].create')
2020-01-06 00:29:42 +13:00
->label('scope', 'public')
2021-03-01 10:22:03 +13:00
->label('auth.type', 'emailPassword')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'createSession')
2020-01-06 00:29:42 +13:00
->label('sdk.description', '/docs/references/account/create-session.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
2020-09-11 02:40:14 +12:00
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('locale')
->inject('geodb')
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($email, $password, $request, $response, $dbForProject, $locale, $geodb, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
2020-07-03 09:48:02 +12:00
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
2020-10-31 08:53:27 +13:00
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Audit $audits */
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
$email = \strtolower($email);
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2021-08-05 17:06:38 +12:00
2022-05-13 04:25:36 +12:00
$profile = $dbForProject->findOne('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
new Query('email', Query::TYPE_EQUAL, [$email])]
);
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) {
throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS); // Wrong password or username
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
if (false === $profile->getAttribute('status')) { // Account is blocked
throw new Exception('Invalid credentials. User is blocked', 401, Exception::USER_BLOCKED); // User is in status blocked
}
2021-02-15 06:28:54 +13:00
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
2020-06-30 09:43:34 +12:00
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$secret = Auth::tokenGenerator();
2021-02-15 06:28:54 +13:00
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
2021-06-13 08:44:25 +12:00
'userId' => $profile->getId(),
2021-02-20 01:12:47 +13:00
'provider' => Auth::SESSION_PROVIDER_EMAIL,
2021-02-19 23:02:02 +13:00
'providerUid' => $email,
2021-02-15 06:28:54 +13:00
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
2021-02-15 21:16:23 +13:00
], $detector->getOS(), $detector->getClient(), $detector->getDevice()
2021-02-15 06:28:54 +13:00
));
2020-10-31 08:53:27 +13:00
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $profile->getId());
2020-01-04 10:00:53 +13:00
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()])
2021-07-17 22:04:43 +12:00
);
2020-01-12 02:58:02 +13:00
2022-04-04 21:59:32 +12:00
$dbForProject->deleteCachedDocument('users', $profile->getId());
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$profile->getId())
2022-04-04 18:30:07 +12:00
->setUser($profile)
2020-06-30 09:43:34 +12:00
;
2020-06-30 09:43:34 +12:00
if (!Config::getParam('domainVerification')) {
2020-01-04 10:00:53 +13:00
$response
2020-06-30 09:43:34 +12:00
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)]))
2020-01-12 02:58:02 +13:00
;
2020-01-04 10:00:53 +13:00
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2020-07-01 18:35:57 +12:00
->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
2020-06-30 09:43:34 +12:00
->setStatusCode(Response::STATUS_CODE_CREATED)
2020-07-03 09:48:02 +12:00
;
2020-10-31 08:53:27 +13:00
2021-07-23 08:15:01 +12:00
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
2021-06-17 21:33:57 +12:00
2020-10-31 08:53:27 +13:00
$session
->setAttribute('current', true)
2021-06-17 21:33:57 +12:00
->setAttribute('countryName', $countryName)
2020-10-31 08:53:27 +13:00
;
2021-08-05 17:06:38 +12:00
2021-08-16 20:53:34 +12:00
$usage
->setParam('users.update', 1)
->setParam('users.sessions.create', 1)
->setParam('provider', 'email')
;
2022-04-04 18:30:07 +12:00
$events
->setParam('userId', $profile->getId())
->setParam('sessionId', $session->getId())
;
2021-07-26 02:47:18 +12:00
$response->dynamic($session, Response::MODEL_SESSION);
2020-12-27 03:31:53 +13:00
});
2020-01-04 10:00:53 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account/sessions/oauth2/:provider')
2020-02-17 00:41:03 +13:00
->desc('Create Account Session with OAuth2')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2021-08-05 17:06:38 +12:00
->label('error', __DIR__ . '/../../views/general/error.phtml')
2020-01-06 00:29:42 +13:00
->label('scope', 'public')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [])
2020-01-06 00:29:42 +13:00
->label('sdk.namespace', 'account')
2020-02-17 00:41:03 +13:00
->label('sdk.method', 'createOAuth2Session')
->label('sdk.description', '/docs/references/account/create-session-oauth2.md')
2021-03-05 18:30:34 +13:00
->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY)
->label('sdk.response.type', Response::CONTENT_TYPE_HTML)
2020-04-11 06:59:14 +12:00
->label('sdk.methodType', 'webAuth')
2020-01-06 00:29:42 +13:00
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
2020-09-11 02:40:14 +12:00
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), function($node) {return (!$node['mock']);}))).'.')
->param('success', '', function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. 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.', true, ['clients'])
->param('failure', '', function ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. 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.', true, ['clients'])
2022-05-01 19:54:58 +12:00
->param('scopes', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 128 characters long.', true)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('project')
->action(function ($provider, $success, $failure, $scopes, $request, $response, $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
2020-01-06 00:29:42 +13:00
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2020-07-03 05:37:24 +12:00
$callback = $protocol.'://'.$request->getHostname().'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId();
2022-04-14 21:54:29 +12:00
$appId = $project->getAttribute('authProviders', [])[$provider.'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider.'Secret'] ?? '{}';
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (!empty($appSecret) && isset($appSecret['version'])) {
2021-08-05 17:06:38 +12:00
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
2020-06-30 09:43:34 +12:00
$appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag']));
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (empty($appId) || empty($appSecret)) {
2022-02-09 10:25:52 +13:00
throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your ' . APP_NAME . ' console to continue.', 412, Exception::PROJECT_PROVIDER_DISABLED);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2021-08-06 22:48:50 +12:00
$className = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider);
2020-06-30 09:43:34 +12:00
2021-08-06 22:48:50 +12:00
if (!\class_exists($className)) {
2022-02-09 10:25:52 +13:00
throw new Exception('Provider is not supported', 501, Exception::PROJECT_PROVIDER_UNSUPPORTED);
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
if(empty($success)) {
$success = $protocol . '://' . $request->getHostname() . $oauthDefaultSuccess;
}
if(empty($failure)) {
$failure = $protocol . '://' . $request->getHostname() . $oauthDefaultFailure;
}
2021-08-06 22:48:50 +12:00
$oauth2 = new $className($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes);
2020-06-30 09:43:34 +12:00
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($oauth2->getLoginURL());
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
2020-02-17 00:41:03 +13:00
->desc('OAuth2 Callback')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2021-08-05 17:06:38 +12:00
->label('error', __DIR__ . '/../../views/general/error.phtml')
2020-01-06 00:29:42 +13:00
->label('scope', 'public')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
2020-09-11 02:40:14 +12:00
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048), 'OAuth2 code.')
2020-09-11 02:40:14 +12:00
->param('state', '', new Text(2048), 'Login state params.', true)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
2020-06-30 23:09:28 +12:00
->action(function ($projectId, $provider, $code, $state, $request, $response) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 23:09:28 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request->getHostname();
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
2021-08-05 17:06:38 +12:00
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state]));
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->desc('OAuth2 Callback')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2021-08-05 17:06:38 +12:00
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('scope', 'public')
->label('origin', '*')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
2020-09-11 02:40:14 +12:00
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
2022-01-23 09:02:51 +13:00
->param('code', '', new Text(2048), 'OAuth2 code.')
2020-09-11 02:40:14 +12:00
->param('state', '', new Text(2048), 'Login state params.', true)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
2020-06-30 23:09:28 +12:00
->action(function ($projectId, $provider, $code, $state, $request, $response) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 23:09:28 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request->getHostname();
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
2021-08-05 17:06:38 +12:00
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state]));
2020-12-27 03:31:53 +13:00
});
2020-06-29 05:31:21 +12:00
App::get('/v1/account/sessions/oauth2/:provider/redirect')
2020-02-17 00:41:03 +13:00
->desc('OAuth2 Redirect')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2021-08-05 17:06:38 +12:00
->label('error', __DIR__ . '/../../views/general/error.phtml')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].create')
2020-01-06 00:29:42 +13:00
->label('scope', 'public')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->label('docs', false)
2020-09-11 02:40:14 +12:00
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
2022-01-23 09:02:51 +13:00
->param('code', '', new Text(2048), 'OAuth2 code.')
2020-09-11 02:40:14 +12:00
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('geodb')
->inject('audits')
->inject('events')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($provider, $code, $state, $request, $response, $project, $user, $dbForProject, $geodb, $audits, $events, $usage) use ($oauthDefaultSuccess) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
2020-10-31 08:53:27 +13:00
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Audit $audits */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2021-08-05 17:06:38 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2021-08-05 17:06:38 +12:00
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
2020-06-30 09:43:34 +12:00
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$validateURL = new URL();
2022-04-14 21:54:29 +12:00
$appId = $project->getAttribute('authProviders', [])[$provider.'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider.'Secret'] ?? '{}';
2020-06-30 09:43:34 +12:00
if (!empty($appSecret) && isset($appSecret['version'])) {
2021-08-05 17:06:38 +12:00
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
2020-06-30 09:43:34 +12:00
$appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag']));
}
2020-01-06 00:29:42 +13:00
2021-08-07 00:30:56 +12:00
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
2020-01-06 00:29:42 +13:00
2021-08-06 22:48:50 +12:00
if (!\class_exists($className)) {
2022-02-09 10:25:52 +13:00
throw new Exception('Provider is not supported', 501, Exception::PROJECT_PROVIDER_UNSUPPORTED);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2021-08-06 22:48:50 +12:00
$oauth2 = new $className($appId, $appSecret, $callback);
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (!empty($state)) {
try {
$state = \array_merge($defaultState, $oauth2->parseState($state));
2021-08-05 17:06:38 +12:00
} catch (\Exception$exception) {
throw new Exception('Failed to parse login state params as passed from OAuth2 provider', 500, Exception::GENERAL_SERVER_ERROR);
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
} else {
$state = $defaultState;
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (!$validateURL->isValid($state['success'])) {
2022-02-09 10:25:52 +13:00
throw new Exception('Invalid redirect URL for success login', 400, Exception::PROJECT_INVALID_SUCCESS_URL);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) {
2022-02-09 10:25:52 +13:00
throw new Exception('Invalid redirect URL for failure login', 400, Exception::PROJECT_INVALID_FAILURE_URL);
2020-06-30 09:43:34 +12:00
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$state['failure'] = null;
2020-06-30 09:43:34 +12:00
$accessToken = $oauth2->getAccessToken($code);
2022-02-01 23:42:11 +13:00
$refreshToken =$oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if (empty($accessToken)) {
if (!empty($state['failure'])) {
$response->redirect($state['failure'], 301, 0);
2020-01-06 00:29:42 +13:00
}
throw new Exception('Failed to obtain access token', 500, Exception::GENERAL_SERVER_ERROR);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$oauth2ID = $oauth2->getUserID($accessToken);
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
if (empty($oauth2ID)) {
if (!empty($state['failure'])) {
$response->redirect($state['failure'], 301, 0);
2020-01-06 00:29:42 +13:00
}
2022-02-09 10:25:52 +13:00
throw new Exception('Missing ID from OAuth2 provider', 400, Exception::PROJECT_MISSING_USER_ID);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2021-05-07 10:31:05 +12:00
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
2020-01-06 00:29:42 +13:00
2021-08-05 17:06:38 +12:00
if ($current) { // Delete current session of new one.
2022-04-04 21:59:32 +12:00
$currentDocument = $dbForProject->getDocument('sessions', $current);
if(!$currentDocument->isEmpty()) {
$dbForProject->deleteDocument('sessions', $currentDocument->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-05-07 10:31:05 +12:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
$user = ($user->isEmpty()) ? $dbForProject->findOne('sessions', [ // Get user by provider id
2021-07-18 09:21:33 +12:00
new Query('provider', QUERY::TYPE_EQUAL, [$provider]),
new Query('providerUid', QUERY::TYPE_EQUAL, [$oauth2ID]),
2021-08-04 08:22:03 +12:00
]) : $user;
2020-01-06 00:29:42 +13:00
2021-07-18 09:21:33 +12:00
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
2020-06-30 09:43:34 +12:00
$name = $oauth2->getUserName($accessToken);
$email = $oauth2->getUserEmail($accessToken);
$isVerified = $oauth2->isEmailVerified($accessToken);
2020-01-06 00:29:42 +13:00
if ($isVerified === true) {
2022-05-13 04:25:36 +12:00
// Get user by email address
$user = $dbForProject->findOne('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
new Query('email', Query::TYPE_EQUAL, [$email])]
);
}
2020-01-06 00:29:42 +13:00
2021-07-18 09:21:33 +12:00
if ($user === false || $user->isEmpty()) { // Last option -> create the user, generate random password
2021-08-06 20:34:17 +12:00
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
2021-08-05 17:06:38 +12:00
2021-03-01 07:36:13 +13:00
if ($limit !== 0) {
2022-05-13 04:25:36 +12:00
$total = $dbForProject->count('users', [new Query('deleted', Query::TYPE_EQUAL, [false])], APP_LIMIT_USERS);
2021-07-18 09:21:33 +12:00
2022-02-27 22:57:09 +13:00
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
2021-03-01 07:36:13 +13:00
}
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
try {
$userId = $dbForProject->getId();
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
2021-05-07 10:31:05 +12:00
'$id' => $userId,
2021-06-12 06:23:16 +12:00
'$read' => ['role:all'],
2021-08-05 17:06:38 +12:00
'$write' => ['user:' . $userId],
2020-06-30 09:43:34 +12:00
'email' => $email,
'emailVerification' => $isVerified,
'status' => true, // Email should already be authenticated by OAuth2 provider
2020-06-30 09:43:34 +12:00
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'passwordUpdate' => 0,
2020-06-30 09:43:34 +12:00
'registration' => \time(),
'reset' => false,
'name' => $name,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass(),
2022-04-26 22:36:49 +12:00
'sessions' => null,
2022-04-27 23:06:53 +12:00
'tokens' => null,
2022-04-28 00:44:47 +12:00
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
])));
2020-06-30 09:43:34 +12:00
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409, Exception::USER_ALREADY_EXISTS);
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception('Invalid credentials. User is blocked', 401, Exception::USER_BLOCKED); // User is in status blocked
}
2020-06-30 09:43:34 +12:00
// Create session token, verify user account and update OAuth2 ID and Access Token
2021-02-15 06:28:54 +13:00
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
2020-06-30 09:43:34 +12:00
$secret = Auth::tokenGenerator();
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
2021-02-15 06:28:54 +13:00
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
2021-06-13 08:44:25 +12:00
'userId' => $user->getId(),
2021-02-19 23:02:02 +13:00
'provider' => $provider,
'providerUid' => $oauth2ID,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => \time() + (int) $accessTokenExpiry,
2020-11-13 00:54:16 +13:00
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
2020-06-30 09:43:34 +12:00
'expire' => $expiry,
2020-07-04 03:14:51 +12:00
'userAgent' => $request->getUserAgent('UNKNOWN'),
2020-06-30 09:43:34 +12:00
'ip' => $request->getIP(),
2021-02-15 06:28:54 +13:00
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
2021-02-15 21:16:23 +13:00
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
2020-10-31 08:53:27 +13:00
2021-02-17 04:51:08 +13:00
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password'));
if ($isAnonymousUser) {
$user
->setAttribute('name', $oauth2->getUserName($accessToken))
->setAttribute('email', $oauth2->getUserEmail($accessToken))
;
}
2020-06-30 09:43:34 +12:00
$user
->setAttribute('status', true)
2020-06-30 09:43:34 +12:00
;
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $user->getId());
2020-06-30 09:43:34 +12:00
2022-04-26 22:46:35 +12:00
$dbForProject->updateDocument('users', $user->getId(), $user);
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
2021-07-18 09:21:33 +12:00
);
2022-04-04 21:59:32 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2020-06-30 09:43:34 +12:00
;
2020-04-09 01:38:36 +12:00
2021-08-16 20:53:34 +12:00
$usage
->setParam('users.sessions.create', 1)
->setParam('projectId', $project->getId())
2021-08-17 00:31:49 +12:00
->setParam('provider', 'oauth2-'.$provider)
2021-08-16 20:53:34 +12:00
;
2022-04-04 18:30:07 +12:00
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
2020-06-30 09:43:34 +12:00
if (!Config::getParam('domainVerification')) {
2022-04-19 21:30:42 +12:00
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
2020-01-06 00:29:42 +13:00
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
// Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing
if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) {
2020-06-30 09:43:34 +12:00
$state['success'] = URLParser::parse($state['success']);
$query = URLParser::parseQuery($state['success']['query']);
$query['project'] = $project->getId();
2020-07-01 18:35:57 +12:00
$query['domain'] = Config::getParam('cookieDomain');
2020-06-30 09:43:34 +12:00
$query['key'] = Auth::$cookieName;
$query['secret'] = Auth::encodeSession($user->getId(), $secret);
$state['success']['query'] = URLParser::unparseQuery($query);
$state['success'] = URLParser::unparse($state['success']);
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2020-07-01 18:35:57 +12:00
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
2020-06-30 09:43:34 +12:00
->redirect($state['success'])
;
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2021-08-30 22:44:52 +12:00
2021-08-31 19:13:30 +12:00
App::post('/v1/account/sessions/magic-url')
->desc('Create Magic URL session')
2021-08-30 22:44:52 +12:00
->groups(['api', 'account'])
2021-08-30 23:09:45 +12:00
->label('scope', 'public')
->label('auth.type', 'magic-url')
2021-08-30 22:44:52 +12:00
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
2021-08-31 19:13:30 +12:00
->label('sdk.method', 'createMagicURLSession')
->label('sdk.description', '/docs/references/account/create-magic-url-session.md')
2021-08-30 22:44:52 +12:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
2021-08-31 17:57:07 +12:00
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "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.')
2021-08-30 22:44:52 +12:00
->param('email', '', new Email(), 'User email.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the magic URL login. 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.', true, ['clients'])
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
2021-08-30 22:44:52 +12:00
->inject('locale')
->inject('audits')
->inject('events')
->inject('mails')
->action(function ($userId, $email, $url, $request, $response, $project, $dbForProject, $locale, $audits, $events, $mails) {
/** @var Appwrite\Utopia\Request $request */
2021-08-30 22:44:52 +12:00
/** @var Appwrite\Utopia\Response $response */
2021-10-08 08:10:43 +13:00
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
2021-08-30 22:44:52 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Audit $audits */
2021-08-30 22:44:52 +12:00
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Mail $mails */
2021-08-30 22:44:52 +12:00
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
2021-08-30 22:44:52 +12:00
$user = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]);
2021-08-30 22:44:52 +12:00
2021-10-08 08:10:43 +13:00
if (!$user) {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
2021-08-30 22:44:52 +12:00
if ($limit !== 0) {
2022-02-27 22:57:09 +13:00
$total = $dbForProject->count('users', [
2021-10-08 08:10:43 +13:00
new Query('deleted', Query::TYPE_EQUAL, [false]),
2022-01-16 12:25:00 +13:00
], APP_LIMIT_USERS);
2021-08-30 22:44:52 +12:00
2022-02-27 22:57:09 +13:00
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
2021-08-30 22:44:52 +12:00
}
}
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
2021-08-30 22:44:52 +12:00
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => null,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass(),
2022-04-26 22:36:49 +12:00
'sessions' => null,
2022-04-27 23:06:53 +12:00
'tokens' => null,
2022-04-28 00:44:47 +12:00
'memberships' => null,
'search' => implode(' ', [$userId, $email]),
'deleted' => false
])));
2021-08-30 22:44:52 +12:00
}
$loginSecret = Auth::tokenGenerator();
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
2021-10-08 08:10:43 +13:00
2021-08-30 22:44:52 +12:00
$token = new Document([
'$id' => $dbForProject->getId(),
2021-08-30 22:44:52 +12:00
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
'secret' => Auth::hash($loginSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
]);
2021-10-08 08:10:43 +13:00
Authorization::setRole('user:'.$user->getId());
2021-08-30 22:44:52 +12:00
2022-04-27 23:06:53 +12:00
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$read', ['user:'.$user->getId()])
->setAttribute('$write', ['user:'.$user->getId()])
);
2021-08-30 22:44:52 +12:00
2022-04-27 23:06:53 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-08-30 22:44:52 +12:00
if(empty($url)) {
2021-09-02 17:21:25 +12:00
$url = $request->getProtocol().'://'.$request->getHostname().'/auth/magic-url';
}
2021-08-30 22:44:52 +12:00
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire, 'project' => $project->getId()]);
2021-08-30 22:44:52 +12:00
$url = Template::unParseURL($url);
$mails
->setType(MAIL_TYPE_MAGIC_SESSION)
->setRecipient($user->getAttribute('email'))
->setUrl($url)
->setLocale($locale->default)
2021-08-30 22:44:52 +12:00
->trigger()
;
2022-05-11 01:52:55 +12:00
$events->setPayload(
$response->output(
2022-04-19 04:21:45 +12:00
$token->setAttribute('secret', $loginSecret),
2021-08-30 22:44:52 +12:00
Response::MODEL_TOKEN
2022-05-11 01:52:55 +12:00
)
);
2021-08-30 22:44:52 +12:00
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $loginSecret : '');
2021-08-30 22:44:52 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2021-08-30 22:44:52 +12:00
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN)
;
});
2021-08-31 19:13:30 +12:00
App::put('/v1/account/sessions/magic-url')
->desc('Create Magic URL session (confirmation)')
2021-08-30 22:44:52 +12:00
->groups(['api', 'account'])
->label('scope', 'public')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].create')
2021-08-30 22:44:52 +12:00
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
2021-08-31 19:13:30 +12:00
->label('sdk.method', 'updateMagicURLSession')
->label('sdk.description', '/docs/references/account/update-magic-url-session.md')
2021-08-30 22:44:52 +12:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new CustomId(), 'User ID.')
2021-08-30 22:44:52 +12:00
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('dbForProject')
2021-08-30 22:44:52 +12:00
->inject('locale')
->inject('geodb')
->inject('audits')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($userId, $secret, $request, $response, $dbForProject, $locale, $geodb, $audits, $events) {
/** @var string $userId */
/** @var string $secret */
/** @var Appwrite\Utopia\Request $request */
2021-08-30 22:44:52 +12:00
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Audit $audits */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2021-08-30 22:44:52 +12:00
2022-04-27 23:06:53 +12:00
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
2021-08-30 22:44:52 +12:00
2021-10-08 08:10:43 +13:00
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
2021-08-30 22:44:52 +12:00
}
2021-10-08 08:10:43 +13:00
$token = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
2021-08-30 22:44:52 +12:00
if (!$token) {
throw new Exception('Invalid login token', 401, Exception::USER_INVALID_TOKEN);
2021-08-30 22:44:52 +12:00
}
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
2021-10-08 08:10:43 +13:00
'userId' => $user->getId(),
2021-08-30 22:44:52 +12:00
'provider' => Auth::SESSION_PROVIDER_MAGIC_URL,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
2021-10-08 08:10:43 +13:00
Authorization::setRole('user:' . $user->getId());
2021-08-30 22:44:52 +12:00
$session = $dbForProject->createDocument('sessions', $session
2021-10-08 08:10:43 +13:00
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
);
2021-08-30 22:44:52 +12:00
2022-04-04 21:59:32 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-10-08 08:10:43 +13:00
$tokens = $user->getAttribute('tokens', []);
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
2022-04-27 23:06:53 +12:00
$dbForProject->deleteDocument('tokens', $token);
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-08-30 22:44:52 +12:00
$user->setAttribute('emailVerification', true);
2021-08-30 22:44:52 +12:00
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
2021-08-30 22:44:52 +12:00
if (false === $user) {
throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR);
2021-08-30 22:44:52 +12:00
}
$audits->setResource('user/'.$user->getId());
2022-04-04 18:30:07 +12:00
$events
2021-08-30 22:44:52 +12:00
->setParam('userId', $user->getId())
2022-04-04 18:30:07 +12:00
->setParam('sessionId', $session->getId())
2021-08-30 22:44:52 +12:00
;
if (!Config::getParam('domainVerification')) {
2022-04-19 21:30:42 +12:00
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
2021-08-30 22:44:52 +12:00
}
$protocol = $request->getProtocol();
$response
->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
2021-08-30 22:44:52 +12:00
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
;
$response->dynamic($session, Response::MODEL_SESSION);
});
2021-02-17 02:46:30 +13:00
App::post('/v1/account/sessions/anonymous')
2021-02-19 03:52:27 +13:00
->desc('Create Anonymous Session')
2021-04-03 21:56:32 +13:00
->groups(['api', 'account', 'auth'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].create')
2021-02-17 02:46:30 +13:00
->label('scope', 'public')
2021-04-03 21:56:32 +13:00
->label('auth.type', 'anonymous')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [])
2021-02-17 02:46:30 +13:00
->label('sdk.namespace', 'account')
2021-02-19 03:52:27 +13:00
->label('sdk.method', 'createAnonymousSession')
->label('sdk.description', '/docs/references/account/create-session-anonymous.md')
2021-02-17 02:46:30 +13:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->inject('request')
->inject('response')
->inject('locale')
->inject('user')
2021-02-24 11:43:05 +13:00
->inject('project')
->inject('dbForProject')
2021-02-17 02:46:30 +13:00
->inject('geodb')
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($request, $response, $locale, $user, $project, $dbForProject, $geodb, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Request $request */
2021-02-17 02:46:30 +13:00
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Locale\Locale $locale */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
2021-02-17 02:46:30 +13:00
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Stats\Stats $events */
2021-02-17 02:46:30 +13:00
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
throw new Exception('Failed to create anonymous user.', 401, Exception::USER_ANONYMOUS_CONSOLE_PROHIBITED);
2021-02-17 02:46:30 +13:00
}
2021-06-12 08:39:00 +12:00
if (!$user->isEmpty()) {
throw new Exception('Cannot create an anonymous user when logged in.', 401, Exception::USER_SESSION_ALREADY_EXISTS);
}
2021-08-06 20:34:17 +12:00
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
2021-04-03 21:56:32 +13:00
if ($limit !== 0) {
2022-02-27 22:57:09 +13:00
$total = $dbForProject->count('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
2022-01-16 12:25:00 +13:00
], APP_LIMIT_USERS);
2021-04-03 21:56:32 +13:00
2022-02-27 22:57:09 +13:00
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
2021-04-03 21:56:32 +13:00
}
}
$userId = $dbForProject->getId();
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
2021-05-10 08:34:32 +12:00
'$id' => $userId,
2021-08-05 17:06:38 +12:00
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
2021-05-10 08:34:32 +12:00
'email' => null,
'emailVerification' => false,
'status' => true,
2021-05-10 08:34:32 +12:00
'password' => null,
'passwordUpdate' => 0,
2021-05-10 08:34:32 +12:00
'registration' => \time(),
'reset' => false,
'name' => null,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass(),
2022-04-26 22:36:49 +12:00
'sessions' => null,
2022-04-27 23:06:53 +12:00
'tokens' => null,
2022-04-28 00:44:47 +12:00
'memberships' => null,
'search' => $userId,
'deleted' => false
])));
2021-02-17 02:46:30 +13:00
2021-02-17 03:16:09 +13:00
// Create session token
2021-02-17 02:46:30 +13:00
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
2021-06-13 08:44:25 +12:00
'userId' => $user->getId(),
2021-03-29 22:16:56 +13:00
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
2021-02-17 02:46:30 +13:00
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $user->getId());
2021-02-17 02:46:30 +13:00
$session = $dbForProject->createDocument('sessions', $session
2021-08-05 17:06:38 +12:00
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
2021-07-17 22:04:43 +12:00
);
2022-04-04 21:59:32 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-02-17 02:46:30 +13:00
$audits->setResource('user/'.$user->getId());
2021-02-17 02:46:30 +13:00
2021-08-16 20:53:34 +12:00
$usage
->setParam('users.sessions.create', 1)
->setParam('provider', 'anonymous')
;
2022-04-04 18:30:07 +12:00
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
2021-02-17 02:46:30 +13:00
if (!Config::getParam('domainVerification')) {
2022-04-19 21:30:42 +12:00
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
2021-02-17 02:46:30 +13:00
}
$response
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2021-02-17 02:46:30 +13:00
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
2021-06-17 21:33:57 +12:00
2021-02-17 02:46:30 +13:00
$session
->setAttribute('current', true)
2021-06-17 21:33:57 +12:00
->setAttribute('countryName', $countryName)
2021-02-17 02:46:30 +13:00
;
2021-07-26 02:47:18 +12:00
$response->dynamic($session, Response::MODEL_SESSION);
2021-02-17 02:46:30 +13:00
});
2020-12-29 09:31:55 +13:00
App::post('/v1/account/jwt')
2020-10-18 06:49:09 +13:00
->desc('Create Account JWT')
2021-03-01 07:36:13 +13:00
->groups(['api', 'account', 'auth'])
2020-10-18 06:49:09 +13:00
->label('scope', 'account')
2021-03-01 07:36:13 +13:00
->label('auth.type', 'jwt')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
2020-10-18 06:49:09 +13:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'createJWT')
->label('sdk.description', '/docs/references/account/create-jwt.md')
2021-05-20 20:52:19 +12:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_JWT)
2020-10-18 06:49:09 +13:00
->label('abuse-limit', 10)
2021-07-27 23:07:39 +12:00
->label('abuse-key', 'url:{url},userId:{userId}')
2020-12-29 06:03:27 +13:00
->inject('response')
->inject('user')
2022-04-04 21:59:32 +12:00
->inject('dbForProject')
->action(function ($response, $user, $dbForProject) {
2020-10-18 06:49:09 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2022-04-04 21:59:32 +12:00
/** @var Utopia\Database\Database $dbForProject */
2021-08-05 17:06:38 +12:00
2020-10-18 06:49:09 +13:00
2022-04-26 20:52:59 +12:00
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$current = $session;
}
}
if ($current->isEmpty()) {
throw new Exception('No valid session found', 404, Exception::USER_SESSION_NOT_FOUND);
2020-12-29 06:03:27 +13:00
}
2021-08-05 17:06:38 +12:00
2020-12-29 10:23:09 +13:00
$jwt = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
2020-12-29 06:03:27 +13:00
2021-05-07 10:31:05 +12:00
$response->setStatusCode(Response::STATUS_CODE_CREATED);
2021-07-26 02:47:18 +12:00
$response->dynamic(new Document(['jwt' => $jwt->encode([
2021-05-07 10:31:05 +12:00
// 'uid' => 1,
// 'aud' => 'http://site.com',
// 'scopes' => ['user'],
// 'iss' => 'http://api.mysite.com',
'userId' => $user->getId(),
'sessionId' => $current->getId(),
])]), Response::MODEL_JWT);
2020-12-29 06:03:27 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account')
2020-02-01 11:34:07 +13:00
->desc('Get Account')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-02-01 11:34:07 +13:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-02-01 11:34:07 +13:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'get')
->label('sdk.description', '/docs/references/account/get.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
2020-12-27 07:11:18 +13:00
->inject('response')
->inject('user')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($response, $user, $usage) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
2022-04-19 21:30:42 +12:00
$usage->setParam('users.read', 1);
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account/prefs')
2020-02-01 11:34:07 +13:00
->desc('Get Account Preferences')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-02-01 11:34:07 +13:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-02-01 11:34:07 +13:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'getPrefs')
->label('sdk.description', '/docs/references/account/get-prefs.md')
2020-11-13 00:54:16 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
2021-04-22 01:37:51 +12:00
->label('sdk.response.model', Response::MODEL_PREFERENCES)
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($response, $user, $usage) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-02-01 11:34:07 +13:00
2021-01-11 00:55:59 +13:00
$prefs = $user->getAttribute('prefs', new \stdClass());
2020-06-30 09:43:34 +12:00
2022-04-19 21:30:42 +12:00
$usage->setParam('users.read', 1);
2021-07-26 02:47:18 +12:00
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account/sessions')
2020-02-01 11:34:07 +13:00
->desc('Get Account Sessions')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-02-01 11:34:07 +13:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-02-01 11:34:07 +13:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'getSessions')
->label('sdk.description', '/docs/references/account/get-sessions.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION_LIST)
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('locale')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($response, $user, $locale, $usage) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2020-06-30 09:43:34 +12:00
/** @var Utopia\Locale\Locale $locale */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
2021-02-20 01:12:47 +13:00
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
2020-06-30 09:43:34 +12:00
2021-08-05 17:06:38 +12:00
foreach ($sessions as $key => $session) {/** @var Document $session */
2021-07-23 08:15:01 +12:00
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
2021-06-17 21:08:01 +12:00
$session->setAttribute('countryName', $countryName);
2021-02-20 01:12:47 +13:00
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
2020-02-01 11:34:07 +13:00
2021-02-20 01:12:47 +13:00
$sessions[$key] = $session;
2020-02-01 11:34:07 +13:00
}
2020-06-30 09:43:34 +12:00
2022-04-19 21:30:42 +12:00
$usage->setParam('users.read', 1);
2021-07-26 02:47:18 +12:00
$response->dynamic(new Document([
2021-05-27 22:09:14 +12:00
'sessions' => $sessions,
2022-02-27 22:57:09 +13:00
'total' => count($sessions),
2020-10-31 08:53:27 +13:00
]), Response::MODEL_SESSION_LIST);
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App::get('/v1/account/logs')
2020-02-01 11:34:07 +13:00
->desc('Get Account Logs')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-02-01 11:34:07 +13:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-02-01 11:34:07 +13:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'getLogs')
->label('sdk.description', '/docs/references/account/get-logs.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
2021-12-15 00:21:44 +13:00
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('locale')
->inject('geodb')
->inject('dbForProject')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($limit, $offset, $response, $user, $locale, $geodb, $dbForProject, $usage) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
2020-06-30 09:43:34 +12:00
/** @var Utopia\Locale\Locale $locale */
2020-10-25 19:15:36 +13:00
/** @var MaxMind\Db\Reader $geodb */
/** @var Utopia\Database\Database $dbForProject */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
$audit = new Audit($dbForProject);
2022-04-04 18:30:07 +12:00
$logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
2020-06-30 09:43:34 +12:00
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
2021-02-15 06:28:54 +13:00
$detector = new Detector($log['userAgent']);
2020-06-30 09:43:34 +12:00
2021-11-18 23:33:42 +13:00
$output[$i] = new Document(array_merge(
$log->getArrayCopy(),
$log['data'],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
2020-10-31 08:53:27 +13:00
$record = $geodb->get($log['ip']);
if ($record) {
2021-07-26 19:05:08 +12:00
$output[$i]['countryCode'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
2021-11-18 23:33:42 +13:00
$output[$i]['countryName'] = $locale->getText('countries.'.strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
2020-10-31 08:53:27 +13:00
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
2020-02-01 11:34:07 +13:00
}
2020-10-31 08:53:27 +13:00
2020-02-01 11:34:07 +13:00
}
2020-06-30 09:43:34 +12:00
2022-04-19 21:30:42 +12:00
$usage->setParam('users.read', 1);
2021-11-17 03:54:29 +13:00
$response->dynamic(new Document([
2022-04-04 18:30:07 +12:00
'total' => $audit->countLogsByUser($user->getId()),
2021-11-17 03:54:29 +13:00
'logs' => $output,
]), Response::MODEL_LOG_LIST);
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2021-06-16 22:14:08 +12:00
App::get('/v1/account/sessions/:sessionId')
->desc('Get Session By ID')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
2021-06-16 22:48:12 +12:00
->label('sdk.method', 'getSession')
->label('sdk.description', '/docs/references/account/get-session.md')
2021-06-16 22:14:08 +12:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
2021-06-16 22:48:12 +12:00
->label('sdk.response.model', Response::MODEL_SESSION)
->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to get the current device session.')
2021-06-16 22:14:08 +12:00
->inject('response')
->inject('user')
->inject('locale')
->inject('dbForProject')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($sessionId, $response, $user, $locale, $dbForProject, $usage) {
2021-06-16 22:14:08 +12:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $user */
2021-06-16 22:14:08 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Utopia\Database\Database $dbForProject */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2021-06-16 22:14:08 +12:00
2021-08-05 17:06:38 +12:00
$sessions = $user->getAttribute('sessions', []);
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
2021-06-16 22:14:08 +12:00
2021-08-05 17:06:38 +12:00
foreach ($sessions as $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
$countryName = $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
2021-06-17 21:33:57 +12:00
$session
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
->setAttribute('countryName', $countryName)
;
2021-08-05 17:06:38 +12:00
2022-04-19 21:30:42 +12:00
$usage->setParam('users.read', 1);
2021-08-16 20:53:34 +12:00
2021-07-26 02:47:18 +12:00
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
2021-06-16 22:48:12 +12:00
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
2021-06-16 22:14:08 +12:00
});
2020-06-29 05:31:21 +12:00
App::patch('/v1/account/name')
2019-05-09 18:54:39 +12:00
->desc('Update Account Name')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].update.name')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2019-05-09 18:54:39 +12:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'updateName')
2020-01-06 12:22:02 +13:00
->label('sdk.description', '/docs/references/account/update-name.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
2020-09-11 02:40:14 +12:00
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($name, $response, $user, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Stats\Stats $events */
2020-06-30 09:43:34 +12:00
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email')]))
);
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2020-06-30 09:43:34 +12:00
;
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
2021-08-16 20:53:34 +12:00
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App::patch('/v1/account/password')
2019-05-09 18:54:39 +12:00
->desc('Update Account Password')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].update.password')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2019-05-09 18:54:39 +12:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'updatePassword')
2020-01-06 12:22:02 +13:00
->label('sdk.description', '/docs/references/account/update-password.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($password, $oldPassword, $response, $user, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Stats\Stats $events */
2020-06-30 09:43:34 +12:00
// 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, Exception::USER_INVALID_CREDENTIALS);
2020-06-30 09:43:34 +12:00
}
2019-05-09 18:54:39 +12:00
2022-04-19 21:30:42 +12:00
$user = $dbForProject->updateDocument(
'users',
$user->getId(),
$user
2021-08-05 17:06:38 +12:00
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('passwordUpdate', \time())
);
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2020-06-30 09:43:34 +12:00
;
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
2022-04-04 18:30:07 +12:00
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App::patch('/v1/account/email')
2019-05-09 18:54:39 +12:00
->desc('Update Account Email')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].update.email')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2019-05-09 18:54:39 +12:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateEmail')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/update-email.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
2020-09-11 02:40:14 +12:00
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($email, $password, $response, $user, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Stats\Stats $events */
2020-06-30 09:43:34 +12:00
2021-02-17 02:46:30 +13:00
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
if (
!$isAnonymousUser &&
!Auth::passwordVerify($password, $user->getAttribute('password'))
) { // Double check user password
throw new Exception('Invalid credentials', 401, Exception::USER_INVALID_CREDENTIALS);
2020-06-30 09:43:34 +12:00
}
2019-07-21 23:43:06 +12:00
$email = \strtolower($email);
$profile = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
2019-05-09 18:54:39 +12:00
2021-05-07 10:31:05 +12:00
if ($profile) {
throw new Exception('User already registered', 409, Exception::USER_ALREADY_EXISTS);
2020-06-30 09:43:34 +12:00
}
2019-05-09 18:54:39 +12:00
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user
2021-08-05 17:06:38 +12:00
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]))
);
} catch(Duplicate $th) {
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
}
2019-05-09 18:54:39 +12:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/'.$user->getId())
2022-04-04 18:30:07 +12:00
->setUser($user)
2020-06-30 09:43:34 +12:00
;
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
2022-04-04 18:30:07 +12:00
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App::patch('/v1/account/prefs')
2020-01-06 00:29:42 +13:00
->desc('Update Account Preferences')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].update.prefs')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2019-05-09 18:54:39 +12:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePrefs')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/update-prefs.md')
2020-11-13 00:54:16 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
2021-04-22 01:37:51 +12:00
->label('sdk.response.model', Response::MODEL_USER)
2020-10-31 08:53:27 +13:00
->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.')
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($prefs, $response, $user, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2019-05-09 18:54:39 +12:00
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
2019-05-09 18:54:39 +12:00
$audits->setResource('user/'.$user->getId());
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
2020-01-12 02:58:02 +13:00
2021-07-26 02:47:18 +12:00
$response->dynamic($user, Response::MODEL_USER);
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App::delete('/v1/account')
2019-05-09 18:54:39 +12:00
->desc('Delete Account')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].delete')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2019-05-09 18:54:39 +12:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'delete')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/delete.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
->inject('events')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($request, $response, $user, $dbForProject, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
2021-05-07 10:31:05 +12:00
// TODO Seems to be related to users.php/App::delete('/v1/users/:userId'). Can we share code between these two? Do todos below apply to users.php?
2021-07-28 02:16:12 +12:00
// TODO delete all tokens or only current session?
// TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later
2022-04-04 18:30:07 +12:00
/**
* Data to delete
* * Tokens
* * Memberships
*/
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
->setResource('user/' . $user->getId())
->setPayload($response->output($user, Response::MODEL_USER))
2020-06-30 09:43:34 +12:00
;
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_USER))
2020-06-30 09:43:34 +12:00
;
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
2019-05-09 18:54:39 +12:00
}
2020-06-30 09:43:34 +12:00
$usage->setParam('users.delete', 1);
2020-06-30 09:43:34 +12:00
$response
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2020-07-01 18:35:57 +12:00
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
2020-06-30 09:43:34 +12:00
->noContent()
;
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App::delete('/v1/account/sessions/:sessionId')
2020-01-06 00:29:42 +13:00
->desc('Delete Account Session')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-01-06 00:29:42 +13:00
->label('scope', 'account')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].delete')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-01-06 00:29:42 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'deleteSession')
2020-01-06 12:07:41 +13:00
->label('sdk.description', '/docs/references/account/delete-session.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
2020-01-06 00:29:42 +13:00
->label('abuse-limit', 100)
->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to delete the current device session.')
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
2021-06-13 08:44:25 +12:00
->inject('locale')
2020-12-27 03:31:53 +13:00
->inject('audits')
->inject('events')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($sessionId, $request, $response, $user, $dbForProject, $locale, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
2021-06-13 08:44:25 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Audit $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2020-06-30 09:43:34 +12:00
$sessionId = ($sessionId === 'current')
2022-02-02 04:54:20 +13:00
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
2021-08-05 17:06:38 +12:00
2021-02-20 01:12:47 +13:00
$sessions = $user->getAttribute('sessions', []);
2020-01-24 11:33:44 +13:00
2021-08-05 17:06:38 +12:00
foreach ($sessions as $key => $session) {/** @var Document $session */
2021-05-07 10:31:05 +12:00
if ($sessionId == $session->getId()) {
unset($sessions[$key]);
2020-01-24 11:33:44 +13:00
$dbForProject->deleteDocument('sessions', $session->getId());
2021-07-17 22:04:43 +12:00
$audits->setResource('user/' . $user->getId());
2020-01-24 11:33:44 +13:00
2021-02-20 01:12:47 +13:00
$session->setAttribute('current', false);
2021-08-05 17:06:38 +12:00
2021-02-20 01:12:47 +13:00
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
2021-06-13 08:44:25 +12:00
$session
->setAttribute('current', true)
->setAttribute('countryName', $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')))
2021-06-13 08:44:25 +12:00
;
2021-08-05 17:06:38 +12:00
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([]))
;
}
2020-11-21 10:02:26 +13:00
2020-01-24 11:33:44 +13:00
$response
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2020-07-01 18:35:57 +12:00
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
2020-01-24 11:33:44 +13:00
;
}
2022-04-26 22:36:49 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-05-07 10:31:05 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
2020-11-21 10:02:26 +13:00
;
2021-08-16 20:53:34 +12:00
$usage
->setParam('users.sessions.delete', 1)
->setParam('users.update', 1)
;
2020-06-30 09:43:34 +12:00
return $response->noContent();
}
}
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
2020-12-27 03:31:53 +13:00
});
2020-06-30 09:43:34 +12:00
App::patch('/v1/account/sessions/:sessionId')
2022-02-04 00:58:21 +13:00
->desc('Update Session (Refresh Tokens)')
2022-02-02 04:54:20 +13:00
->groups(['api', 'account'])
->label('scope', 'account')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].update')
2022-02-02 04:54:20 +13:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateSession')
->label('sdk.description', '/docs/references/account/update-session.md')
2022-02-02 04:54:20 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('audits')
->inject('events')
->inject('usage')
->action(function ($sessionId, $request, $response, $user, $dbForProject, $project, $locale, $audits, $events, $usage) {
2022-02-02 04:54:20 +13:00
/** @var Appwrite\Utopia\Request $request */
/** @var boolean $force */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Audit $audits */
2022-02-02 04:54:20 +13:00
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
2022-02-02 05:47:08 +13:00
2022-02-02 04:54:20 +13:00
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
2022-04-14 21:54:29 +12:00
$appId = $project->getAttribute('authProviders', [])[$provider.'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider.'Secret'] ?? '{}';
2022-02-02 04:54:20 +13:00
$className = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider);
2022-02-04 00:57:04 +13:00
if (!\class_exists($className)) {
2022-02-28 00:57:41 +13:00
throw new Exception('Provider is not supported', 501, Exception::PROJECT_PROVIDER_UNSUPPORTED);
2022-02-04 00:57:04 +13:00
}
2022-02-02 04:54:20 +13:00
$oauth2 = new $className($appId, $appSecret, '', [], []);
$oauth2->refreshTokens($refreshToken);
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
2022-02-04 05:04:46 +13:00
->setAttribute('providerAccessTokenExpiry', \time() + (int) $oauth2->getAccessTokenExpiry(''))
2022-02-02 04:54:20 +13:00
;
2022-02-02 05:30:49 +13:00
$dbForProject->updateDocument('sessions', $sessionId, $session);
2022-04-26 22:36:49 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2022-02-02 04:54:20 +13:00
$audits->setResource('user/' . $user->getId());
2022-02-02 04:54:20 +13:00
$events
2022-04-04 18:30:07 +12:00
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
2022-02-02 04:54:20 +13:00
;
$usage
->setParam('users.sessions.update', 1)
->setParam('users.update', 1)
;
2022-02-02 05:47:08 +13:00
return $response->dynamic($session, Response::MODEL_SESSION);
2022-02-02 04:54:20 +13:00
}
}
2022-02-28 00:57:41 +13:00
throw new Exception('Session not found', 404, Exception::USER_SESSION_NOT_FOUND);
2022-02-02 04:54:20 +13:00
});
2020-06-30 09:43:34 +12:00
App::delete('/v1/account/sessions')
->desc('Delete All Account Sessions')
->groups(['api', 'account'])
->label('scope', 'account')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].sessions.[sessionId].delete')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-06-30 09:43:34 +12:00
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteSessions')
->label('sdk.description', '/docs/references/account/delete-sessions.md')
2020-11-12 10:02:24 +13:00
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
2020-06-30 09:43:34 +12:00
->label('abuse-limit', 100)
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
2021-06-13 08:44:25 +12:00
->inject('locale')
2020-12-27 03:31:53 +13:00
->inject('audits')
->inject('events')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($request, $response, $user, $dbForProject, $locale, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
2021-06-13 08:44:25 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Audit $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request->getProtocol();
2021-02-20 02:59:36 +13:00
$sessions = $user->getAttribute('sessions', []);
2020-06-30 09:43:34 +12:00
2021-08-05 17:06:38 +12:00
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
2020-06-30 09:43:34 +12:00
$audits->setResource('user/' . $user->getId());
2020-06-30 09:43:34 +12:00
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([]))
;
}
2021-06-13 08:44:25 +12:00
$session
->setAttribute('current', false)
->setAttribute('countryName', $locale->getText('countries.'.strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')))
2021-06-13 08:44:25 +12:00
;
2020-11-21 10:02:26 +13:00
2021-02-20 02:59:36 +13:00
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$session->setAttribute('current', true);
2020-06-30 09:43:34 +12:00
$response
2021-08-05 17:06:38 +12:00
->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
2020-07-01 18:35:57 +12:00
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
2020-06-30 09:43:34 +12:00
;
}
2020-01-24 11:33:44 +13:00
}
2021-05-07 10:31:05 +12:00
2022-04-26 22:36:49 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2021-08-05 17:06:38 +12:00
2021-08-17 17:23:43 +12:00
$numOfSessions = count($sessions);
2020-12-07 11:14:57 +13:00
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
2022-04-04 18:30:07 +12:00
->setPayload($response->output(new Document([
2021-05-27 22:09:14 +12:00
'sessions' => $sessions,
2022-02-27 22:57:09 +13:00
'total' => $numOfSessions,
2020-11-21 10:02:26 +13:00
]), Response::MODEL_SESSION_LIST))
;
2020-06-30 09:43:34 +12:00
2021-08-16 20:53:34 +12:00
$usage
2021-08-17 17:23:43 +12:00
->setParam('users.sessions.delete', $numOfSessions)
2021-08-16 20:53:34 +12:00
->setParam('users.update', 1)
;
2020-06-30 09:43:34 +12:00
$response->noContent();
2020-12-27 03:31:53 +13:00
});
2020-01-24 11:33:44 +13:00
2020-06-29 05:31:21 +12:00
App::post('/v1/account/recovery')
2020-02-09 07:51:49 +13:00
->desc('Create Password Recovery')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-01-06 12:07:41 +13:00
->label('scope', 'public')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].recovery.[tokenId].create')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-01-06 12:07:41 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'createRecovery')
2020-01-06 12:07:41 +13:00
->label('sdk.description', '/docs/references/account/create-recovery.md')
2020-11-13 11:50:53 +13:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
2020-01-06 12:07:41 +13:00
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'ip:{ip}'])
2020-09-11 02:40:14 +12:00
->param('email', '', new Email(), 'User email.')
2021-08-05 17:06:38 +12:00
->param('url', '', function ($clients) {return new Host($clients);}, 'URL to redirect the user back to your app from the recovery 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'])
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('project')
->inject('locale')
->inject('mails')
->inject('audits')
->inject('events')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($email, $url, $request, $response, $dbForProject, $project, $locale, $mails, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
2020-06-30 09:43:34 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Mail $mails */
/** @var Appwrite\Event\Audit $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-11-21 01:35:16 +13:00
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
2020-06-30 09:43:34 +12:00
$email = \strtolower($email);
2022-05-13 04:25:36 +12:00
$profile = $dbForProject->findOne('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
new Query('email', Query::TYPE_EQUAL, [$email])
]);
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
if (!$profile) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
2020-06-30 09:43:34 +12:00
}
2020-01-12 02:58:02 +13:00
if (false === $profile->getAttribute('status')) { // Account is blocked
throw new Exception('Invalid credentials. User is blocked', 401, Exception::USER_BLOCKED);
}
2021-07-07 00:18:55 +12:00
$expire = \time() + Auth::TOKEN_EXPIRATION_RECOVERY;
2020-06-30 09:43:34 +12:00
$secret = Auth::tokenGenerator();
$recovery = new Document([
'$id' => $dbForProject->getId(),
2021-06-13 08:44:25 +12:00
'userId' => $profile->getId(),
2020-06-30 09:43:34 +12:00
'type' => Auth::TOKEN_TYPE_RECOVERY,
2020-11-13 00:54:16 +13:00
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
2021-07-07 00:18:55 +12:00
'expire' => $expire,
2020-07-04 03:14:51 +12:00
'userAgent' => $request->getUserAgent('UNKNOWN'),
2020-06-30 09:43:34 +12:00
'ip' => $request->getIP(),
]);
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $profile->getId());
2020-01-12 02:58:02 +13:00
2022-04-27 23:06:53 +12:00
$recovery = $dbForProject->createDocument('tokens', $recovery
->setAttribute('$read', ['user:'.$profile->getId()])
->setAttribute('$write', ['user:'.$profile->getId()])
);
2020-01-06 12:07:41 +13:00
2022-04-27 23:06:53 +12:00
$dbForProject->deleteCachedDocument('users', $profile->getId());
2020-06-30 09:43:34 +12:00
$url = Template::parseURL($url);
2021-07-07 00:18:55 +12:00
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret, 'expire' => $expire]);
2020-06-30 09:43:34 +12:00
$url = Template::unParseURL($url);
2020-07-06 02:19:59 +12:00
$mails
->setType(MAIL_TYPE_RECOVERY)
->setRecipient($profile->getAttribute('email', ''))
->setUrl($url)
->setLocale($locale->default)
->setName($profile->getAttribute('name'))
2020-06-30 09:43:34 +12:00
->trigger();
;
2020-12-07 11:14:57 +13:00
$events
->setParam('userId', $profile->getId())
->setParam('tokenId', $recovery->getId())
2022-04-19 04:21:45 +12:00
->setUser($profile)
->setPayload($response->output(
$recovery->setAttribute('secret', $secret),
Response::MODEL_TOKEN
))
2020-11-19 08:38:31 +13:00
;
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$recovery->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
2020-11-19 08:38:31 +13:00
$audits->setResource('user/' . $profile->getId());
2022-04-19 04:21:45 +12:00
$usage->setParam('users.update', 1);
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
$response->setStatusCode(Response::STATUS_CODE_CREATED);
2021-07-26 02:47:18 +12:00
$response->dynamic($recovery, Response::MODEL_TOKEN);
2020-12-27 03:31:53 +13:00
});
2020-01-06 12:07:41 +13:00
2020-06-29 05:31:21 +12:00
App::put('/v1/account/recovery')
2021-08-31 19:13:30 +12:00
->desc('Create Password Recovery (confirmation)')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-01-06 12:07:41 +13:00
->label('scope', 'public')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].recovery.[tokenId].update')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-01-06 12:07:41 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'updateRecovery')
2020-01-06 12:07:41 +13:00
->label('sdk.description', '/docs/references/account/update-recovery.md')
2020-11-13 11:50:53 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
2020-01-06 12:07:41 +13:00
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new UID(), 'User ID.')
2020-09-11 02:40:14 +12:00
->param('secret', '', new Text(256), 'Valid reset token.')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($userId, $secret, $password, $passwordAgain, $response, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
if ($password !== $passwordAgain) {
throw new Exception('Passwords must match', 400, Exception::USER_PASSWORD_MISMATCH);
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
$profile = $dbForProject->getDocument('users', $userId);
2020-01-06 12:07:41 +13:00
if ($profile->isEmpty() || $profile->getAttribute('deleted')) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
2021-05-07 10:31:05 +12:00
$tokens = $profile->getAttribute('tokens', []);
$recovery = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_RECOVERY, $secret);
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if (!$recovery) {
throw new Exception('Invalid recovery token', 401, Exception::USER_INVALID_TOKEN);
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $profile->getId());
2020-01-06 12:07:41 +13:00
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
2021-08-05 17:06:38 +12:00
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('passwordUpdate', \time())
->setAttribute('emailVerification', true)
2021-05-07 10:31:05 +12:00
);
2020-01-06 12:07:41 +13:00
2022-04-27 23:06:53 +12:00
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
2020-06-30 09:43:34 +12:00
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
2022-04-27 23:06:53 +12:00
$dbForProject->deleteDocument('tokens', $recovery);
$dbForProject->deleteCachedDocument('users', $profile->getId());
2021-05-07 10:31:05 +12:00
$audits->setResource('user/' . $profile->getId());
2020-01-06 12:07:41 +13:00
$usage->setParam('users.update', 1);
2022-04-04 18:30:07 +12:00
$events
2020-06-30 09:43:34 +12:00
->setParam('userId', $profile->getId())
2022-05-09 03:49:17 +12:00
->setParam('tokenId', $recoveryDocument->getId())
2020-06-30 09:43:34 +12:00
;
2020-01-06 12:07:41 +13:00
2022-04-27 23:06:53 +12:00
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
2020-12-27 03:31:53 +13:00
});
2020-01-12 13:20:35 +13:00
2020-06-29 05:31:21 +12:00
App::post('/v1/account/verification')
2020-02-10 10:37:28 +13:00
->desc('Create Email Verification')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-01-12 13:20:35 +13:00
->label('scope', 'account')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].verification.[tokenId].create')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-01-12 13:20:35 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'createVerification')
2020-01-12 13:20:35 +13:00
->label('sdk.description', '/docs/references/account/create-verification.md')
2020-11-13 11:50:53 +13:00
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
2020-01-12 13:20:35 +13:00
->label('abuse-limit', 10)
2021-07-27 23:21:26 +12:00
->label('abuse-key', 'url:{url},userId:{userId}')
2020-06-30 09:43:34 +12:00
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification 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 built-in confirm page
2020-12-27 03:31:53 +13:00
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('locale')
->inject('audits')
->inject('events')
->inject('mails')
2021-08-16 20:53:34 +12:00
->inject('usage')
->action(function ($url, $request, $response, $project, $user, $dbForProject, $locale, $audits, $events, $mails, $usage) {
/** @var Appwrite\Utopia\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-07-26 02:47:18 +12:00
/** @var Utopia\Database\Document $project */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
2020-06-30 09:43:34 +12:00
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Audit $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Mail $mails */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2020-11-21 01:35:16 +13:00
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
2020-06-30 09:43:34 +12:00
$verificationSecret = Auth::tokenGenerator();
2021-07-07 00:18:55 +12:00
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$verification = new Document([
'$id' => $dbForProject->getId(),
2021-06-13 08:44:25 +12:00
'userId' => $user->getId(),
2020-06-30 09:43:34 +12:00
'type' => Auth::TOKEN_TYPE_VERIFICATION,
2020-11-13 00:54:16 +13:00
'secret' => Auth::hash($verificationSecret), // One way hash encryption to protect DB leak
2021-07-07 00:18:55 +12:00
'expire' => $expire,
2020-07-04 03:14:51 +12:00
'userAgent' => $request->getUserAgent('UNKNOWN'),
2020-06-30 09:43:34 +12:00
'ip' => $request->getIP(),
]);
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $user->getId());
2020-01-12 13:20:35 +13:00
2022-04-27 23:06:53 +12:00
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$read', ['user:'.$user->getId()])
->setAttribute('$write', ['user:'.$user->getId()])
);
2020-01-12 13:20:35 +13:00
2022-04-27 23:06:53 +12:00
$dbForProject->deleteCachedDocument('users', $user->getId());
2020-06-30 09:43:34 +12:00
$url = Template::parseURL($url);
2021-07-07 00:18:55 +12:00
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret, 'expire' => $expire]);
2020-06-30 09:43:34 +12:00
$url = Template::unParseURL($url);
2020-07-06 02:19:59 +12:00
$mails
->setType(MAIL_TYPE_VERIFICATION)
->setRecipient($user->getAttribute('email'))
->setUrl($url)
->setLocale($locale->default)
->setName($user->getAttribute('name'))
2020-06-30 09:43:34 +12:00
->trigger()
;
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
->setParam('userId', $user->getId())
->setParam('tokenId', $verification->getId())
->setPayload($response->output(
$verification->setAttribute('secret', $verificationSecret),
Response::MODEL_TOKEN
))
2020-11-19 08:38:31 +13:00
;
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
2020-11-19 08:38:31 +13:00
$audits->setResource('user/' . $user->getId());
$usage->setParam('users.update', 1);
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
$response->setStatusCode(Response::STATUS_CODE_CREATED);
2021-07-26 02:47:18 +12:00
$response->dynamic($verification, Response::MODEL_TOKEN);
2020-12-27 03:31:53 +13:00
});
2020-01-12 13:20:35 +13:00
2020-06-29 05:31:21 +12:00
App::put('/v1/account/verification')
2021-08-31 19:13:30 +12:00
->desc('Create Email Verification (confirmation)')
2020-06-26 06:32:12 +12:00
->groups(['api', 'account'])
2020-01-12 13:20:35 +13:00
->label('scope', 'public')
2022-04-04 18:30:07 +12:00
->label('event', 'users.[userId].verification.[tokenId].update')
2021-04-16 19:22:17 +12:00
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
2020-01-12 13:20:35 +13:00
->label('sdk.namespace', 'account')
2020-01-31 05:18:46 +13:00
->label('sdk.method', 'updateVerification')
2020-01-12 13:20:35 +13:00
->label('sdk.description', '/docs/references/account/update-verification.md')
2020-11-13 11:50:53 +13:00
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
2020-01-12 13:20:35 +13:00
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new UID(), 'User ID.')
2020-09-11 02:40:14 +12:00
->param('secret', '', new Text(256), 'Valid verification token.')
2020-12-27 03:31:53 +13:00
->inject('response')
->inject('user')
->inject('dbForProject')
2020-12-27 03:31:53 +13:00
->inject('audits')
2021-08-16 20:53:34 +12:00
->inject('usage')
2022-04-04 18:30:07 +12:00
->inject('events')
->action(function ($userId, $secret, $response, $user, $dbForProject, $audits, $usage, $events) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2021-05-07 10:31:05 +12:00
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Appwrite\Event\Audit $audits */
2021-08-16 20:53:34 +12:00
/** @var Appwrite\Stats\Stats $usage */
2022-04-04 18:30:07 +12:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
2022-04-27 23:06:53 +12:00
$profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
if ($profile->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
2020-06-30 09:43:34 +12:00
}
2020-01-12 13:20:35 +13:00
2021-05-07 10:31:05 +12:00
$tokens = $profile->getAttribute('tokens', []);
$verification = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_VERIFICATION, $secret);
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
if (!$verification) {
throw new Exception('Invalid verification token', 401, Exception::USER_INVALID_TOKEN);
2020-06-30 09:43:34 +12:00
}
2020-01-12 13:20:35 +13:00
2021-08-05 17:06:38 +12:00
Authorization::setRole('user:' . $profile->getId());
2020-01-12 13:20:35 +13:00
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
2020-01-12 13:20:35 +13:00
2022-04-27 23:06:53 +12:00
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
/**
* We act like we're updating and validating
* the verification token but actually we don't need it anymore.
*/
2022-04-27 23:06:53 +12:00
$dbForProject->deleteDocument('tokens', $verification);
$dbForProject->deleteCachedDocument('users', $profile->getId());
2021-05-07 10:31:05 +12:00
$audits->setResource('user/' . $user->getId());
2020-01-12 13:20:35 +13:00
$usage->setParam('users.update', 1);
2022-04-04 18:30:07 +12:00
$events
->setParam('userId', $user->getId())
->setParam('tokenId', $verificationDocument->getId())
2021-08-16 20:53:34 +12:00
;
2022-04-04 18:30:07 +12:00
2022-04-27 23:06:53 +12:00
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});