diff --git a/app/config/auth.php b/app/config/auth.php index 11b89dd9ec..170b87344e 100644 --- a/app/config/auth.php +++ b/app/config/auth.php @@ -10,6 +10,13 @@ return [ 'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateSession', 'enabled' => true, ], + 'magic-url' => [ + 'name' => 'Magic URL', + 'key' => 'usersAuthMagicURL', + 'icon' => '/images/users/magic-url.png', + 'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateMagicURLSession', + 'enabled' => true, + ], 'anonymous' => [ 'name' => 'Anonymous', 'key' => 'usersAuthAnonymous', diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index de6d8bb718..c00740b130 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -9,6 +9,12 @@ "emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.", "emails.verification.thanks": "Thanks", "emails.verification.signature": "{{project}} team", + "emails.magicSession.subject": "Login", + "emails.magicSession.hello": "Hey,", + "emails.magicSession.body": "Follow this link to login.", + "emails.magicSession.footer": "If you didn’t ask to login using this email, you can ignore this message.", + "emails.magicSession.thanks": "Thanks", + "emails.magicSession.signature": "{{project}} team", "emails.recovery.subject": "Password Reset", "emails.recovery.hello": "Hello {{name}}", "emails.recovery.body": "Follow this link to reset your {{project}} password.", diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 55bcde6ca0..d007ea1fa2 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -611,6 +611,291 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); + +App::post('/v1/account/sessions/magic-url') + ->desc('Create Magic URL session') + ->groups(['api', 'account']) + ->label('scope', 'public') + ->label('auth.type', 'magic-url') + ->label('sdk.auth', []) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createMagicURLSession') + ->label('sdk.description', '/docs/references/account/create-magic-url-session.md') + ->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) + ->label('abuse-key', 'url:{url},email:{param-email}') + ->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('projectDB') + ->inject('locale') + ->inject('audits') + ->inject('events') + ->inject('mails') + ->action(function ($email, $url, $request, $response, $project, $projectDB, $locale, $audits, $events, $mails) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $events */ + /** @var Appwrite\Event\Event $mails */ + + if(empty(App::getEnv('_APP_SMTP_HOST'))) { + throw new Exception('SMTP Disabled', 503); + } + + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); + $isAppUser = Auth::isAppUser(Authorization::$roles); + + $user = $projectDB->getCollectionFirst([ // Get user by email address + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + 'email='.$email, + ], + ]); + + if (empty($user)) { + $limit = $project->getAttribute('usersAuthLimit', 0); + + if ($limit !== 0) { + $projectDB->getCollection([ // Count users + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + ], + ]); + + $sum = $projectDB->getSum(); + + if($sum >= $limit) { + throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501); + } + } + + Authorization::disable(); + + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => null, + 'passwordUpdate' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => null, + ], ['email' => $email]); + + Authorization::reset(); + $mails->setParam('event', 'users.create'); + $audits->setParam('event', 'users.create'); + } + + $loginSecret = Auth::tokenGenerator(); + + $expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM; + + $token = new Document([ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], + '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(), + ]); + + Authorization::setRole('user:'.$user->getId()); + + $token = $projectDB->createDocument($token->getArrayCopy()); + + if (false === $token) { + throw new Exception('Failed saving token to DB', 500); + } + + $user->setAttribute('tokens', $token, Document::SET_TYPE_APPEND); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed to save user to DB', 500); + } + + if(empty($url)) { + $url = $request->getProtocol().'://'.$request->getHostname().'/auth/magic-url'; + } + + $url = Template::parseURL($url); + $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire, 'project' => $project->getId()]); + $url = Template::unParseURL($url); + + $mails + ->setParam('from', $project->getId()) + ->setParam('recipient', $user->getAttribute('email')) + ->setParam('url', $url) + ->setParam('locale', $locale->default) + ->setParam('project', $project->getAttribute('name', ['[APP-NAME]'])) + ->setParam('type', MAIL_TYPE_MAGIC_SESSION) + ->trigger() + ; + + $events + ->setParam('eventData', + $response->output($token->setAttribute('secret', $loginSecret), + Response::MODEL_TOKEN + )) + ; + + $token // Hide secret for clients + ->setAttribute('secret', + ($isPrivilegedUser || $isAppUser) ? $loginSecret : ''); + + $audits + ->setParam('userId', $user->getId()) + ->setParam('resource', 'users/'.$user->getId()) + ; + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($token, Response::MODEL_TOKEN) + ; + }); + +App::put('/v1/account/sessions/magic-url') + ->desc('Create Magic URL session (confirmation)') + ->groups(['api', 'account']) + ->label('scope', 'public') + ->label('event', 'account.sessions.create') + ->label('sdk.auth', []) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updateMagicURLSession') + ->label('sdk.description', '/docs/references/account/update-magic-url-session.md') + ->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 UID(), 'User unique ID.') + ->param('secret', '', new Text(256), 'Valid verification token.') + ->inject('request') + ->inject('response') + ->inject('projectDB') + ->inject('locale') + ->inject('geodb') + ->inject('audits') + ->action(function ($userId, $secret, $request, $response, $projectDB, $locale, $geodb, $audits) { + /** @var string $userId */ + /** @var string $secret */ + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Locale\Locale $locale */ + /** @var MaxMind\Db\Reader $geodb */ + /** @var Appwrite\Event\Event $audits */ + + $profile = $projectDB->getCollectionFirst([ // Get user by user ID + 'limit' => 1, + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + '$id='.$userId, + ], + ]); + + if (empty($profile)) { + throw new Exception('User not found', 404); + } + + $token = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret); + + if (!$token) { + throw new Exception('Invalid login token', 401); + } + + $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( + [ + '$collection' => Database::SYSTEM_COLLECTION_SESSIONS, + '$permissions' => ['read' => ['user:' . $profile->getId()], 'write' => ['user:' . $profile->getId()]], + 'userId' => $profile->getId(), + '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() + )); + + Authorization::setRole('user:'.$profile->getId()); + + $session = $projectDB->createDocument($session->getArrayCopy()); + + if (false === $session) { + throw new Exception('Failed saving session to DB', 500); + } + + $profile->setAttribute('emailVerification', true); + $profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); + + $user = $projectDB->updateDocument($profile->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + if (!$projectDB->deleteDocument($token)) { + throw new Exception('Failed to remove login token from DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ; + } + + $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 = (isset($countries[strtoupper($session->getAttribute('countryCode'))])) + ? $countries[strtoupper($session->getAttribute('countryCode'))] + : $locale->getText('locale.country.unknown'); + + $session + ->setAttribute('current', true) + ->setAttribute('countryName', $countryName) + ; + + $response->dynamic($session, Response::MODEL_SESSION); + }); + App::post('/v1/account/sessions/anonymous') ->desc('Create Anonymous Session') ->groups(['api', 'account', 'auth']) @@ -1461,6 +1746,10 @@ App::post('/v1/account/recovery') /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $events */ + if(empty(App::getEnv('_APP_SMTP_HOST'))) { + throw new Exception('SMTP Disabled', 503); + } + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); $isAppUser = Auth::isAppUser(Authorization::$roles); @@ -1551,7 +1840,7 @@ App::post('/v1/account/recovery') }); App::put('/v1/account/recovery') - ->desc('Complete Password Recovery') + ->desc('Create Password Recovery (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'account.recovery.update') @@ -1664,6 +1953,10 @@ App::post('/v1/account/verification') /** @var Appwrite\Event\Event $events */ /** @var Appwrite\Event\Event $mails */ + if(empty(App::getEnv('_APP_SMTP_HOST'))) { + throw new Exception('SMTP Disabled', 503); + } + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); $isAppUser = Auth::isAppUser(Authorization::$roles); @@ -1738,7 +2031,7 @@ App::post('/v1/account/verification') }); App::put('/v1/account/verification') - ->desc('Complete Email Verification') + ->desc('Create Email Verification (confirmation)') ->groups(['api', 'account']) ->label('scope', 'public') ->label('event', 'account.verification.update') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 05b8a1542f..7421d7a885 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -274,6 +274,10 @@ App::post('/v1/teams/:teamId/memberships') /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $mails */ + if(empty(App::getEnv('_APP_SMTP_HOST'))) { + throw new Exception('SMTP Disabled', 503); + } + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); $isAppUser = Auth::isAppUser(Authorization::$roles); diff --git a/app/controllers/general.php b/app/controllers/general.php index 0af6d92fd4..09631cf0a4 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -337,6 +337,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { case 412: // Error allowed publicly case 429: // Error allowed publicly case 501: // Error allowed publicly + case 503: // Error allowed publicly $code = $error->getCode(); $message = $error->getMessage(); break; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0df72d0578..c3134b4f86 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -141,6 +141,12 @@ App::init(function ($utopia, $request, $response, $project, $user) { } break; + case 'magic-url': + if($project->getAttribute('usersAuthMagicURL', true) === false) { + throw new Exception('Magic URL authentication is disabled for this project', 501); + } + break; + case 'anonymous': if($project->getAttribute('usersAuthAnonymous', true) === false) { throw new Exception('Anonymous authentication is disabled for this project', 501); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 8017eda767..69d42b8aa4 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -326,6 +326,7 @@ App::get('/console/users') $page ->setParam('auth', Config::getParam('auth')) ->setParam('providers', Config::getParam('providers')) + ->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST')))) ; $layout diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index f3eeb71c94..51c7c52064 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -197,6 +197,24 @@ App::get('/auth/oauth2/success') ; }); +App::get('/auth/magic-url') + ->groups(['web', 'home']) + ->label('permission', 'public') + ->label('scope', 'home') + ->inject('layout') + ->action(function ($layout) { + /** @var Utopia\View $layout */ + + $page = new View(__DIR__.'/../../views/home/auth/magicURL.phtml'); + + $layout + ->setParam('title', APP_NAME) + ->setParam('body', $page) + ->setParam('header', []) + ->setParam('footer', []) + ; + }); + App::get('/auth/oauth2/failure') ->groups(['web', 'home']) ->label('permission', 'public') diff --git a/app/init.php b/app/init.php index df71d60de1..4b8f78fc25 100644 --- a/app/init.php +++ b/app/init.php @@ -72,6 +72,7 @@ const DELETE_TYPE_ABUSE = 'abuse'; const DELETE_TYPE_CERTIFICATES = 'certificates'; // Mail Types const MAIL_TYPE_VERIFICATION = 'verification'; +const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; const MAIL_TYPE_RECOVERY = 'recovery'; const MAIL_TYPE_INVITATION = 'invitation'; // Auth Types diff --git a/app/views/console/users/index.phtml b/app/views/console/users/index.phtml index 65b611be21..3e62f772bb 100644 --- a/app/views/console/users/index.phtml +++ b/app/views/console/users/index.phtml @@ -1,6 +1,7 @@ getParam('providers', []); $auth = $this->getParam('auth', []); +$smtpEnabled = $this->getParam('smtpEnabled', false); ?>
@@ -372,19 +373,26 @@ $auth = $this->getParam('auth', []); data-failure-param-alert-text="Failed to update project auth status settings" data-failure-param-alert-classname="error"> - - + + + - + Email/Password Logo - escape($name); ?> soon - - -

- Docs -

+ escape($name); ?> soon + + +

+ SMTP Disabled +

+ + +

+ Docs +

+
diff --git a/app/views/home/auth/magicURL.phtml b/app/views/home/auth/magicURL.phtml new file mode 100644 index 0000000000..d3cae68858 --- /dev/null +++ b/app/views/home/auth/magicURL.phtml @@ -0,0 +1,38 @@ + + + +
\ No newline at end of file diff --git a/app/workers/mails.php b/app/workers/mails.php index d72ca35724..453dcad8fe 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -42,15 +42,14 @@ class MailsV1 extends Worker $body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl'); $subject = ''; switch ($type) { - case MAIL_TYPE_RECOVERY: - $subject = $locale->getText("$prefix.subject"); - break; case MAIL_TYPE_INVITATION: $subject = \sprintf($locale->getText("$prefix.subject"), $this->args['team'], $project); $body->setParam('{{owner}}', $this->args['owner']); $body->setParam('{{team}}', $this->args['team']); break; + case MAIL_TYPE_RECOVERY: case MAIL_TYPE_VERIFICATION: + case MAIL_TYPE_MAGIC_SESSION: $subject = $locale->getText("$prefix.subject"); break; default: @@ -127,6 +126,8 @@ class MailsV1 extends Worker return 'emails.invitation'; case MAIL_TYPE_VERIFICATION: return 'emails.verification'; + case MAIL_TYPE_MAGIC_SESSION: + return 'emails.magicSession'; default: throw new Exception('Undefined Mail Type : ' . $type, 500); } diff --git a/docs/references/account/create-magic-url-session.md b/docs/references/account/create-magic-url-session.md new file mode 100644 index 0000000000..4be53f1381 --- /dev/null +++ b/docs/references/account/create-magic-url-session.md @@ -0,0 +1 @@ +Sends the user an email with a secret key for creating a session. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default. \ No newline at end of file diff --git a/docs/references/account/update-magic-url-session.md b/docs/references/account/update-magic-url-session.md new file mode 100644 index 0000000000..b85036082f --- /dev/null +++ b/docs/references/account/update-magic-url-session.md @@ -0,0 +1,3 @@ +Use this endpoint to complete creating the session with the Magic URL. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession) endpoint. + +Please note that in order to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface. \ No newline at end of file diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index e4db3f821f..b9e75f8b81 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2239,7 +2239,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;iclient->call(Client::METHOD_POST, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + // 'url' => 'http://localhost/magiclogin', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEmpty($response['body']['secret']); + $this->assertIsNumeric($response['body']['expire']); + + $userId = $response['body']['userId']; + + $lastEmail = $this->getLastEmail(); + $this->assertEquals($email, $lastEmail['to'][0]['address']); + $this->assertEquals('Login', $lastEmail['subject']); + + $token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); + + $expireTime = strpos($lastEmail['text'], 'expire='.$response['body']['expire'], 0); + + $this->assertNotFalse($expireTime); + + $secretTest = strpos($lastEmail['text'], 'secret='.$response['body']['secret'], 0); + + $this->assertNotFalse($secretTest); + + $userIDTest = strpos($lastEmail['text'], 'userId='.$response['body']['userId'], 0); + + $this->assertNotFalse($userIDTest); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'url' => 'localhost/magiclogin', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'url' => 'http://remotehost/magiclogin', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $data['token'] = $token; + $data['id'] = $userId; + + return $data; + } + + /** + * @depends testCreateMagicUrl + */ + public function testCreateSessionWithMagicUrl($data):array + { + $id = $data['id'] ?? ''; + $token = $data['token'] ?? ''; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $id, + 'secret' => $token, + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['userId']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => 'ewewe', + 'secret' => $token, + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => $id, + 'secret' => 'sdasdasdasd', + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + return $data; + } } \ No newline at end of file