Implement OTP email endpoint + tests
This commit is contained in:
parent
31a67a7667
commit
df9bc6df56
7 changed files with 354 additions and 5 deletions
|
@ -17,6 +17,13 @@ return [
|
||||||
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateMagicURLToken',
|
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateMagicURLToken',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
|
'email' => [
|
||||||
|
'name' => 'Email (OTP)',
|
||||||
|
'key' => 'email',
|
||||||
|
'icon' => '/images/users/email.png',
|
||||||
|
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateEmailToken',
|
||||||
|
'enabled' => true,
|
||||||
|
],
|
||||||
'anonymous' => [
|
'anonymous' => [
|
||||||
'name' => 'Anonymous',
|
'name' => 'Anonymous',
|
||||||
'key' => 'anonymous',
|
'key' => 'anonymous',
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<style>
|
<style>
|
||||||
a { color:currentColor; word-break: break-all; }
|
a { color:currentColor; word-break: break-all; }
|
||||||
body {
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
color: #616B7C;
|
color: #616B7C;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
|
@ -2,9 +2,19 @@
|
||||||
|
|
||||||
<p>{{description}}</p>
|
<p>{{description}}</p>
|
||||||
|
|
||||||
<p>{{otp}}</p>
|
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; display: inline-block;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="border-radius: 8px; background-color: #ffffff;">
|
||||||
|
<p style="font-size: 32px; text-indent: 18px; letter-spacing: 18px; font-family: Inter; color: #414146; text-decoration: none; border-radius: 8px; padding: 32px 16px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold; ">{{otp}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<p>{{clientInfo}}</p>
|
<p>{{clientInfo}}</p>
|
||||||
|
|
||||||
<p style="margin-bottom: 0px;">{{thanks}}</p>
|
<p style="margin-bottom: 0px;">{{thanks}}</p>
|
||||||
<p style="margin-top: 0px;">{{signature}}</p>
|
<p style="margin-top: 0px;">{{signature}}</p>
|
||||||
|
|
||||||
|
<hr style="margin-block-start: 1rem; margin-block-end: 1rem;">
|
||||||
|
|
||||||
|
<p style="opacity: 0.7;">{{securityPhrase}}</p>
|
|
@ -20,8 +20,9 @@
|
||||||
"emails.magicSession.signature": "{{project}} team",
|
"emails.magicSession.signature": "{{project}} team",
|
||||||
"emails.otpSession.subject": "{{project}} Login",
|
"emails.otpSession.subject": "{{project}} Login",
|
||||||
"emails.otpSession.hello": "Hello,",
|
"emails.otpSession.hello": "Hello,",
|
||||||
"emails.otpSession.optionButton": "Enter the following verification code when prompted to securely sign in to your {{projectBold}} account. It will expire in 15 minutes.",
|
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{projectBold}} account. It will expire in 15 minutes.",
|
||||||
"emails.otpSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.",
|
"emails.otpSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.",
|
||||||
|
"emails.otpSession.securityPhrase": "Security phrase for this email is {{phrase}}. You can trust this email if this phrase matches the phrase shown during sign in.",
|
||||||
"emails.otpSession.thanks": "Thanks,",
|
"emails.otpSession.thanks": "Thanks,",
|
||||||
"emails.otpSession.signature": "{{project}} team",
|
"emails.otpSession.signature": "{{project}} team",
|
||||||
"emails.recovery.subject": "Password Reset",
|
"emails.recovery.subject": "Password Reset",
|
||||||
|
|
|
@ -1204,6 +1204,235 @@ App::post('/v1/account/tokens/magic-url')
|
||||||
;
|
;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
App::post('/v1/account/tokens/email')
|
||||||
|
->desc('Create email token (OTP)')
|
||||||
|
->groups(['api', 'account'])
|
||||||
|
->label('scope', 'sessions.write')
|
||||||
|
->label('auth.type', 'email')
|
||||||
|
->label('audits.event', 'session.create')
|
||||||
|
->label('audits.resource', 'user/{response.userId}')
|
||||||
|
->label('audits.userId', '{response.userId}')
|
||||||
|
->label('sdk.auth', [])
|
||||||
|
->label('sdk.namespace', 'account')
|
||||||
|
->label('sdk.method', 'createEmailToken')
|
||||||
|
->label('sdk.description', '/docs/references/account/create-token-email.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('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||||
|
->param('email', '', new Email(), 'User email.')
|
||||||
|
->param('securityPhrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of authentication flow.', true)
|
||||||
|
->inject('request')
|
||||||
|
->inject('response')
|
||||||
|
->inject('user')
|
||||||
|
->inject('project')
|
||||||
|
->inject('dbForProject')
|
||||||
|
->inject('locale')
|
||||||
|
->inject('queueForEvents')
|
||||||
|
->inject('queueForMails')
|
||||||
|
->action(function (string $userId, string $email, bool $securityPhrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
|
||||||
|
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||||
|
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($securityPhrase === true) {
|
||||||
|
$securityPhrase = SecurityPhrase::generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
$roles = Authorization::getRoles();
|
||||||
|
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||||
|
$isAppUser = Auth::isAppUser($roles);
|
||||||
|
|
||||||
|
$result = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
|
||||||
|
if ($result !== false && !$result->isEmpty()) {
|
||||||
|
$user->setAttributes($result->getArrayCopy());
|
||||||
|
} else {
|
||||||
|
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
|
||||||
|
|
||||||
|
if ($limit !== 0) {
|
||||||
|
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
|
||||||
|
|
||||||
|
if ($total >= $limit) {
|
||||||
|
throw new Exception(Exception::USER_COUNT_EXCEEDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure this email is not already used in another identity
|
||||||
|
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||||
|
Query::equal('providerEmail', [$email]),
|
||||||
|
]);
|
||||||
|
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
|
||||||
|
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||||
|
|
||||||
|
$user->setAttributes([
|
||||||
|
'$id' => $userId,
|
||||||
|
'$permissions' => [
|
||||||
|
Permission::read(Role::any()),
|
||||||
|
Permission::update(Role::user($userId)),
|
||||||
|
Permission::delete(Role::user($userId)),
|
||||||
|
],
|
||||||
|
'email' => $email,
|
||||||
|
'emailVerification' => false,
|
||||||
|
'status' => true,
|
||||||
|
'password' => null,
|
||||||
|
'hash' => Auth::DEFAULT_ALGO,
|
||||||
|
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||||
|
'passwordUpdate' => null,
|
||||||
|
'registration' => DateTime::now(),
|
||||||
|
'reset' => false,
|
||||||
|
'prefs' => new \stdClass(),
|
||||||
|
'sessions' => null,
|
||||||
|
'tokens' => null,
|
||||||
|
'memberships' => null,
|
||||||
|
'search' => implode(' ', [$userId, $email]),
|
||||||
|
'accessedAt' => DateTime::now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->removeAttribute('$internalId');
|
||||||
|
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenSecret = Auth::codeGenerator(6);
|
||||||
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP));
|
||||||
|
|
||||||
|
$token = new Document([
|
||||||
|
'$id' => ID::unique(),
|
||||||
|
'userId' => $user->getId(),
|
||||||
|
'userInternalId' => $user->getInternalId(),
|
||||||
|
'type' => Auth::TOKEN_TYPE_EMAIL,
|
||||||
|
'secret' => Auth::hash($tokenSecret), // One way hash encryption to protect DB leak
|
||||||
|
'expire' => $expire,
|
||||||
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
|
'ip' => $request->getIP(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Authorization::setRole(Role::user($user->getId())->toString());
|
||||||
|
|
||||||
|
$token = $dbForProject->createDocument('tokens', $token
|
||||||
|
->setAttribute('$permissions', [
|
||||||
|
Permission::read(Role::user($user->getId())),
|
||||||
|
Permission::update(Role::user($user->getId())),
|
||||||
|
Permission::delete(Role::user($user->getId())),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||||
|
|
||||||
|
$subject = $locale->getText("emails.otpSession.subject");
|
||||||
|
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||||
|
|
||||||
|
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||||
|
$agentOs = $detector->getOS();
|
||||||
|
$agentClient = $detector->getClient();
|
||||||
|
$agentDevice = $detector->getDevice();
|
||||||
|
|
||||||
|
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-otp.tpl');
|
||||||
|
$message
|
||||||
|
->setParam('{{hello}}', $locale->getText("emails.otpSession.hello"))
|
||||||
|
->setParam('{{description}}', $locale->getText("emails.otpSession.description"))
|
||||||
|
->setParam('{{clientInfo}}', $locale->getText("emails.otpSession.clientInfo"))
|
||||||
|
->setParam('{{thanks}}', $locale->getText("emails.otpSession.thanks"))
|
||||||
|
->setParam('{{signature}}', $locale->getText("emails.otpSession.signature"));
|
||||||
|
|
||||||
|
if (!empty($securityPhrase)) {
|
||||||
|
$message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase"));
|
||||||
|
} else {
|
||||||
|
$message->setParam('{{securityPhrase}}', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $message->render();
|
||||||
|
|
||||||
|
$smtp = $project->getAttribute('smtp', []);
|
||||||
|
$smtpEnabled = $smtp['enabled'] ?? false;
|
||||||
|
|
||||||
|
$senderEmail = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||||
|
$senderName = App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||||
|
$replyTo = "";
|
||||||
|
|
||||||
|
if ($smtpEnabled) {
|
||||||
|
if (!empty($smtp['senderEmail'])) {
|
||||||
|
$senderEmail = $smtp['senderEmail'];
|
||||||
|
}
|
||||||
|
if (!empty($smtp['senderName'])) {
|
||||||
|
$senderName = $smtp['senderName'];
|
||||||
|
}
|
||||||
|
if (!empty($smtp['replyTo'])) {
|
||||||
|
$replyTo = $smtp['replyTo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$queueForMails
|
||||||
|
->setSmtpHost($smtp['host'] ?? '')
|
||||||
|
->setSmtpPort($smtp['port'] ?? '')
|
||||||
|
->setSmtpUsername($smtp['username'] ?? '')
|
||||||
|
->setSmtpPassword($smtp['password'] ?? '')
|
||||||
|
->setSmtpSecure($smtp['secure'] ?? '');
|
||||||
|
|
||||||
|
if (!empty($customTemplate)) {
|
||||||
|
if (!empty($customTemplate['senderEmail'])) {
|
||||||
|
$senderEmail = $customTemplate['senderEmail'];
|
||||||
|
}
|
||||||
|
if (!empty($customTemplate['senderName'])) {
|
||||||
|
$senderName = $customTemplate['senderName'];
|
||||||
|
}
|
||||||
|
if (!empty($customTemplate['replyTo'])) {
|
||||||
|
$replyTo = $customTemplate['replyTo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $customTemplate['message'] ?? '';
|
||||||
|
$subject = $customTemplate['subject'] ?? $subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
$queueForMails
|
||||||
|
->setSmtpReplyTo($replyTo)
|
||||||
|
->setSmtpSenderEmail($senderEmail)
|
||||||
|
->setSmtpSenderName($senderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$emailVariables = [
|
||||||
|
'direction' => $locale->getText('settings.direction'),
|
||||||
|
/* {{user}} ,{{team}}, {{project}} and {{otp}} are required in the templates */
|
||||||
|
'user' => '',
|
||||||
|
'team' => '',
|
||||||
|
'project' => $project->getAttribute('name'),
|
||||||
|
'projectBold' => '<strong>' . $project->getAttribute('name') . '</strong>',
|
||||||
|
'otp' => $tokenSecret,
|
||||||
|
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
|
||||||
|
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
|
||||||
|
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
|
||||||
|
'phrase' => '<strong>' . (!empty($securityPhrase) ? $securityPhrase : '') . '</strong>'
|
||||||
|
];
|
||||||
|
|
||||||
|
$queueForMails
|
||||||
|
->setSubject($subject)
|
||||||
|
->setBody($body)
|
||||||
|
->setVariables($emailVariables)
|
||||||
|
->setRecipient($email)
|
||||||
|
->trigger();
|
||||||
|
|
||||||
|
$queueForEvents->setPayload(
|
||||||
|
$response->output(
|
||||||
|
$token->setAttribute('secret', $tokenSecret),
|
||||||
|
Response::MODEL_TOKEN
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hide secret for clients
|
||||||
|
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $tokenSecret : '');
|
||||||
|
|
||||||
|
if (!empty($securityPhrase)) {
|
||||||
|
$token->setAttribute('securityPhrase', $securityPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response
|
||||||
|
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||||
|
->dynamic($token, Response::MODEL_TOKEN)
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
||||||
$roles = Authorization::getRoles();
|
$roles = Authorization::getRoles();
|
||||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||||
|
@ -1457,7 +1686,7 @@ App::post('/v1/account/tokens/phone')
|
||||||
}
|
}
|
||||||
|
|
||||||
$secret = Auth::codeGenerator();
|
$secret = Auth::codeGenerator();
|
||||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE));
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP));
|
||||||
|
|
||||||
$token = new Document([
|
$token = new Document([
|
||||||
'$id' => ID::unique(),
|
'$id' => ID::unique(),
|
||||||
|
|
|
@ -54,6 +54,7 @@ class Auth
|
||||||
public const TOKEN_TYPE_PHONE = 6;
|
public const TOKEN_TYPE_PHONE = 6;
|
||||||
public const TOKEN_TYPE_OAUTH2 = 7;
|
public const TOKEN_TYPE_OAUTH2 = 7;
|
||||||
public const TOKEN_TYPE_GENERIC = 8;
|
public const TOKEN_TYPE_GENERIC = 8;
|
||||||
|
public const TOKEN_TYPE_EMAIL = 9; // OTP
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session Providers.
|
* Session Providers.
|
||||||
|
@ -73,7 +74,7 @@ class Auth
|
||||||
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
|
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
|
||||||
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
||||||
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
|
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
|
||||||
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
|
public const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
|
||||||
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
|
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -129,4 +129,104 @@ trait AccountBase
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEmailOTPSession(): void
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Test for SUCCESS
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => 'otpuser@appwrite.io'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($response['headers']['status-code'], 201);
|
||||||
|
$this->assertNotEmpty($response['body']['$id']);
|
||||||
|
$this->assertNotEmpty($response['body']['$createdAt']);
|
||||||
|
$this->assertNotEmpty($response['body']['userId']);
|
||||||
|
$this->assertNotEmpty($response['body']['expire']);
|
||||||
|
$this->assertEmpty($response['body']['secret']);
|
||||||
|
$this->assertEmpty($response['body']['securityPhrase']);
|
||||||
|
|
||||||
|
$userId = $response['body']['userId'];
|
||||||
|
|
||||||
|
$lastEmail = $this->getLastEmail();
|
||||||
|
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
|
||||||
|
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||||
|
|
||||||
|
// FInd 6 concurrent digits in email text - OTP
|
||||||
|
preg_match_all("/\b\d{6}\b/", $lastEmail['text'], $matches);
|
||||||
|
$code = ($matches[0] ?? [])[0] ?? '';
|
||||||
|
|
||||||
|
$this->assertNotEmpty($code);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => $userId,
|
||||||
|
'secret' => $code
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals($userId, $response['body']['userId']);
|
||||||
|
$this->assertNotEmpty($response['body']['$id']);
|
||||||
|
$this->assertNotEmpty($response['body']['expire']);
|
||||||
|
$this->assertEmpty($response['body']['secret']);
|
||||||
|
|
||||||
|
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals($userId, $response['body']['$id']);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => $userId,
|
||||||
|
'secret' => $code
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(401, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals('user_invalid_token', $response['body']['type']);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => 'otpuser@appwrite.io',
|
||||||
|
'securityPhrase' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals($response['headers']['status-code'], 201);
|
||||||
|
$this->assertNotEmpty($response['body']['securityPhrase']);
|
||||||
|
$this->assertEmpty($response['body']['secret']);
|
||||||
|
$this->assertEquals($userId, $response['body']['userId']);
|
||||||
|
|
||||||
|
$securityPhrase = $response['body']['securityPhrase'];
|
||||||
|
|
||||||
|
$lastEmail = $this->getLastEmail();
|
||||||
|
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
|
||||||
|
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||||
|
$this->assertStringContainsStringIgnoringCase('security phrase', $lastEmail['text']);
|
||||||
|
$this->assertStringContainsStringIgnoringCase($securityPhrase, $lastEmail['text']);
|
||||||
|
|
||||||
|
// TODO: Delete user
|
||||||
|
// TODO: Failure tests (both endpoints);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue