Merge pull request #3357 from appwrite/feat-phone-authentication
feat: phone authentication
This commit is contained in:
commit
e6edcb5459
55 changed files with 1921 additions and 135 deletions
2
.env
2
.env
|
@ -56,6 +56,8 @@ _APP_SMTP_PORT=1025
|
|||
_APP_SMTP_SECURE=
|
||||
_APP_SMTP_USERNAME=
|
||||
_APP_SMTP_PASSWORD=
|
||||
_APP_PHONE_PROVIDER=phone://mock
|
||||
_APP_PHONE_FROM=
|
||||
_APP_STORAGE_LIMIT=30000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
|
|
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
|
@ -41,6 +41,7 @@ jobs:
|
|||
- name: Teardown
|
||||
if: always()
|
||||
run: |
|
||||
docker compose down -v
|
||||
docker ps -aq | xargs docker rm --force
|
||||
docker volume prune --force
|
||||
docker network prune --force
|
|
@ -193,6 +193,10 @@ ENV _APP_SERVER=swoole \
|
|||
_APP_SMTP_SECURE= \
|
||||
_APP_SMTP_USERNAME= \
|
||||
_APP_SMTP_PASSWORD= \
|
||||
_APP_PHONE_PROVIDER= \
|
||||
_APP_PHONE_USER= \
|
||||
_APP_PHONE_KEY= \
|
||||
_APP_PHONE_FROM= \
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
|
||||
_APP_FUNCTIONS_TIMEOUT=900 \
|
||||
_APP_FUNCTIONS_CONTAINERS=10 \
|
||||
|
@ -305,6 +309,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
|||
chmod +x /usr/local/bin/worker-functions && \
|
||||
chmod +x /usr/local/bin/worker-builds && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-webhooks
|
||||
|
||||
# Letsencrypt Permissions
|
||||
|
|
|
@ -7,7 +7,7 @@ return [
|
|||
'name' => 'Email/Password',
|
||||
'key' => 'emailPassword',
|
||||
'icon' => '/images/users/email.png',
|
||||
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateSession',
|
||||
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateEmailSession',
|
||||
'enabled' => true,
|
||||
],
|
||||
'magic-url' => [
|
||||
|
@ -42,7 +42,7 @@ return [
|
|||
'name' => 'Phone',
|
||||
'key' => 'phone',
|
||||
'icon' => '/images/users/phone.png',
|
||||
'docs' => '',
|
||||
'enabled' => false,
|
||||
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreatePhoneSession',
|
||||
'enabled' => true,
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1025,6 +1025,17 @@ $collections = [
|
|||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'phone',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16, // leading '+' and 15 digitts maximum by E.164 format
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'status',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
|
@ -1091,6 +1102,17 @@ $collections = [
|
|||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'phoneVerification',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'reset',
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -389,6 +389,30 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'Phone',
|
||||
'description' => '',
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_PHONE_PROVIDER',
|
||||
'description' => 'Provider used for delivering SMS for Phone authentication. Use the following format: \'phone://[USER]:[SECRET]@[PROVIDER]\'. \n\nAvailable providers are twilio, text-magic and telesign.',
|
||||
'introduction' => '0.15.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_PHONE_FROM',
|
||||
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
|
||||
'introduction' => '0.15.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'Storage',
|
||||
'description' => '',
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Phone;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
use Appwrite\Auth\Validator\Phone as ValidatorPhone;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
|
@ -19,6 +21,7 @@ use Appwrite\Utopia\Database\Validator\CustomId;
|
|||
use MaxMind\Db\Reader;
|
||||
use Utopia\App;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Phone as EventPhone;
|
||||
use Utopia\Audit\Audit as EventAudit;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
|
@ -129,16 +132,16 @@ App::post('/v1/account')
|
|||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions')
|
||||
->desc('Create Account Session')
|
||||
App::post('/v1/account/sessions/email')
|
||||
->desc('Create Account Session with Email')
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'emailPassword')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createSession')
|
||||
->label('sdk.description', '/docs/references/account/create-session.md')
|
||||
->label('sdk.method', 'createEmailSession')
|
||||
->label('sdk.description', '/docs/references/account/create-session-email.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SESSION)
|
||||
|
@ -521,7 +524,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
||||
|
||||
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password'));
|
||||
$isAnonymousUser = Auth::isAnonymousUser($user);
|
||||
|
||||
if ($isAnonymousUser) {
|
||||
$user
|
||||
|
@ -830,6 +833,232 @@ App::put('/v1/account/sessions/magic-url')
|
|||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/phone')
|
||||
->desc('Create Phone session')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('auth.type', 'phone')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createPhoneSession')
|
||||
->label('sdk.description', '/docs/references/account/create-phone-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('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.')
|
||||
->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('audits')
|
||||
->inject('events')
|
||||
->inject('messaging')
|
||||
->inject('phone')
|
||||
->action(function (string $userId, string $number, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, EventPhone $messaging, Phone $phone) {
|
||||
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
|
||||
throw new Exception('Phone Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
|
||||
}
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
|
||||
$user = $dbForProject->findOne('users', [new Query('phone', Query::TYPE_EQUAL, [$number])]);
|
||||
|
||||
if (!$user) {
|
||||
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
|
||||
|
||||
if ($limit !== 0) {
|
||||
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
|
||||
|
||||
if ($total >= $limit) {
|
||||
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
|
||||
|
||||
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
|
||||
'$id' => $userId,
|
||||
'$read' => ['role:all'],
|
||||
'$write' => ['user:' . $userId],
|
||||
'email' => null,
|
||||
'phone' => $number,
|
||||
'emailVerification' => false,
|
||||
'phoneVerification' => false,
|
||||
'status' => true,
|
||||
'password' => null,
|
||||
'passwordUpdate' => 0,
|
||||
'registration' => \time(),
|
||||
'reset' => false,
|
||||
'prefs' => new \stdClass(),
|
||||
'sessions' => null,
|
||||
'tokens' => null,
|
||||
'memberships' => null,
|
||||
'search' => implode(' ', [$userId, $number])
|
||||
])));
|
||||
}
|
||||
|
||||
$secret = $phone->generateSecretDigits();
|
||||
|
||||
$expire = \time() + Auth::TOKEN_EXPIRATION_PHONE;
|
||||
|
||||
$token = new Document([
|
||||
'$id' => $dbForProject->getId(),
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_PHONE,
|
||||
'secret' => $secret,
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole('user:' . $user->getId());
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token
|
||||
->setAttribute('$read', ['user:' . $user->getId()])
|
||||
->setAttribute('$write', ['user:' . $user->getId()]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
|
||||
$messaging
|
||||
->setRecipient($number)
|
||||
->setMessage($secret)
|
||||
->trigger();
|
||||
|
||||
$events->setPayload(
|
||||
$response->output(
|
||||
$token->setAttribute('secret', $secret),
|
||||
Response::MODEL_TOKEN
|
||||
)
|
||||
);
|
||||
|
||||
// Hide secret for clients
|
||||
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
|
||||
|
||||
$audits
|
||||
->setResource('user/' . $user->getId())
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($token, Response::MODEL_TOKEN)
|
||||
;
|
||||
});
|
||||
|
||||
App::put('/v1/account/sessions/phone')
|
||||
->desc('Create Phone session (confirmation)')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePhoneSession')
|
||||
->label('sdk.description', '/docs/references/account/update-phone-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 CustomId(), 'User ID.')
|
||||
->param('secret', '', new Text(256), 'Valid verification token.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('audits')
|
||||
->inject('events')
|
||||
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) {
|
||||
|
||||
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$token = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
|
||||
|
||||
if (!$token) {
|
||||
throw new Exception('Invalid login token', 401, Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
$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(),
|
||||
'userId' => $user->getId(),
|
||||
'provider' => Auth::SESSION_PROVIDER_PHONE,
|
||||
'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:' . $user->getId());
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session
|
||||
->setAttribute('$read', ['user:' . $user->getId()])
|
||||
->setAttribute('$write', ['user:' . $user->getId()]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
|
||||
/**
|
||||
* We act like we're updating and validating
|
||||
* the recovery token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $token);
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
|
||||
$user->setAttribute('phoneVerification', true);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
if (false === $user) {
|
||||
throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$audits->setResource('user/' . $user->getId());
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->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 = $locale->getText('countries.' . strtolower($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'])
|
||||
|
@ -1292,7 +1521,7 @@ App::patch('/v1/account/email')
|
|||
->inject('events')
|
||||
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
|
||||
|
||||
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
|
||||
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
|
||||
|
||||
if (
|
||||
!$isAnonymousUser &&
|
||||
|
@ -1302,18 +1531,15 @@ App::patch('/v1/account/email')
|
|||
}
|
||||
|
||||
$email = \strtolower($email);
|
||||
$profile = $dbForProject->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
|
||||
|
||||
if ($profile) {
|
||||
throw new Exception('User already registered', 409, Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user
|
||||
$user
|
||||
->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')])));
|
||||
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]));
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
@ -1329,6 +1555,59 @@ App::patch('/v1/account/email')
|
|||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/account/phone')
|
||||
->desc('Update Account Phone')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].update.phone')
|
||||
->label('scope', 'account')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePhone')
|
||||
->label('sdk.description', '/docs/references/account/update-phone.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->action(function (string $phone, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
|
||||
|
||||
$isAnonymousUser = Auth::isAnonymousUser($user); // 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);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $phone)
|
||||
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
|
||||
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]));
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception('Phone number already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$audits
|
||||
->setResource('user/' . $user->getId())
|
||||
->setUser($user)
|
||||
;
|
||||
|
||||
$usage->setParam('users.update', 1);
|
||||
$events->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/account/prefs')
|
||||
->desc('Update Account Preferences')
|
||||
->groups(['api', 'account'])
|
||||
|
@ -1813,7 +2092,7 @@ App::post('/v1/account/verification')
|
|||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createVerification')
|
||||
->label('sdk.description', '/docs/references/account/create-verification.md')
|
||||
->label('sdk.description', '/docs/references/account/create-email-verification.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
|
@ -1903,7 +2182,7 @@ App::put('/v1/account/verification')
|
|||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updateVerification')
|
||||
->label('sdk.description', '/docs/references/account/update-verification.md')
|
||||
->label('sdk.description', '/docs/references/account/update-email-verification.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
|
@ -1956,3 +2235,146 @@ App::put('/v1/account/verification')
|
|||
|
||||
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::post('/v1/account/verification/phone')
|
||||
->desc('Create Phone Verification')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('event', 'users.[userId].verification.[tokenId].create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createPhoneVerification')
|
||||
->label('sdk.description', '/docs/references/account/create-phone-verification.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', 'userId:{userId}')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('phone')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('audits')
|
||||
->inject('events')
|
||||
->inject('usage')
|
||||
->inject('messaging')
|
||||
->action(function (Request $request, Response $response, Phone $phone, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage, EventPhone $messaging) {
|
||||
|
||||
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception('SMTP Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('phone'))) {
|
||||
throw new Exception('User has no phone number.', 400);
|
||||
}
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
|
||||
$verificationSecret = Auth::tokenGenerator();
|
||||
|
||||
$secret = $phone->generateSecretDigits();
|
||||
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
|
||||
|
||||
$verification = new Document([
|
||||
'$id' => $dbForProject->getId(),
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_PHONE,
|
||||
'secret' => $secret,
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
]);
|
||||
|
||||
Authorization::setRole('user:' . $user->getId());
|
||||
|
||||
$verification = $dbForProject->createDocument('tokens', $verification
|
||||
->setAttribute('$read', ['user:' . $user->getId()])
|
||||
->setAttribute('$write', ['user:' . $user->getId()]));
|
||||
|
||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||
|
||||
$messaging
|
||||
->setRecipient($user->getAttribute('phone'))
|
||||
->setMessage($secret);
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('tokenId', $verification->getId())
|
||||
->setPayload($response->output(
|
||||
$verification->setAttribute('secret', $verificationSecret),
|
||||
Response::MODEL_TOKEN
|
||||
))
|
||||
;
|
||||
|
||||
// Hide secret for clients
|
||||
$verification->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $verificationSecret : '');
|
||||
|
||||
$audits->setResource('user/' . $user->getId());
|
||||
$usage->setParam('users.update', 1);
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($verification, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::put('/v1/account/verification/phone')
|
||||
->desc('Create Phone Verification (confirmation)')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'public')
|
||||
->label('event', 'users.[userId].verification.[tokenId].update')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'updatePhoneVerification')
|
||||
->label('sdk.description', '/docs/references/account/update-phone-verification.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'userId:{param-userId}')
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('secret', '', new Text(256), 'Valid verification token.')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('audits')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->action(function (string $userId, string $secret, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
|
||||
|
||||
$profile = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
if ($profile->isEmpty()) {
|
||||
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$verification = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
|
||||
|
||||
if (!$verification) {
|
||||
throw new Exception('Invalid verification token', 401, Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
Authorization::setRole('user:' . $profile->getId());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
|
||||
|
||||
$verificationDocument = $dbForProject->getDocument('tokens', $verification);
|
||||
|
||||
/**
|
||||
* We act like we're updating and validating the verification token but actually we don't need it anymore.
|
||||
*/
|
||||
$dbForProject->deleteDocument('tokens', $verification);
|
||||
$dbForProject->deleteCachedDocument('users', $profile->getId());
|
||||
|
||||
$audits->setResource('user/' . $user->getId());
|
||||
|
||||
$usage->setParam('users.update', 1);
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('tokenId', $verificationDocument->getId())
|
||||
;
|
||||
|
||||
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
|
@ -406,8 +407,8 @@ App::patch('/v1/users/:userId/verification')
|
|||
->label('scope', 'users.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updateVerification')
|
||||
->label('sdk.description', '/docs/references/users/update-user-verification.md')
|
||||
->label('sdk.method', 'updateEmailVerification')
|
||||
->label('sdk.description', '/docs/references/users/update-user-email-verification.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
|
@ -438,6 +439,45 @@ App::patch('/v1/users/:userId/verification')
|
|||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/verification/phone')
|
||||
->desc('Update Phone Verification')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.verification')
|
||||
->label('scope', 'users.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updatePhoneVerification')
|
||||
->label('sdk.description', '/docs/references/users/update-user-phone-verification.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('phoneVerification', false, new Boolean(), 'User phone verification status.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('usage')
|
||||
->inject('events')
|
||||
->action(function (string $userId, bool $phoneVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
|
||||
|
||||
$usage
|
||||
->setParam('users.update', 1)
|
||||
;
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId())
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/name')
|
||||
->desc('Update Name')
|
||||
->groups(['api', 'users'])
|
||||
|
@ -551,15 +591,11 @@ App::patch('/v1/users/:userId/email')
|
|||
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
|
||||
if (!$isAnonymousUser) {
|
||||
//TODO: Remove previous unique ID.
|
||||
}
|
||||
|
||||
$email = \strtolower($email);
|
||||
|
||||
$user
|
||||
->setAttribute('email', $email)
|
||||
->setAttribute('emailVerification', false)
|
||||
->setAttribute('search', \implode(' ', [$user->getId(), $email, $user->getAttribute('name')]))
|
||||
;
|
||||
|
||||
|
@ -570,6 +606,55 @@ App::patch('/v1/users/:userId/email')
|
|||
}
|
||||
|
||||
|
||||
$audits
|
||||
->setResource('user/' . $user->getId())
|
||||
;
|
||||
|
||||
$events
|
||||
->setParam('userId', $user->getId())
|
||||
;
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::patch('/v1/users/:userId/phone')
|
||||
->desc('Update Phone')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.phone')
|
||||
->label('scope', 'users.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'updatePhone')
|
||||
->label('sdk.description', '/docs/references/users/update-user-phone.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('number', '', new Phone(), 'User phone number.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('audits')
|
||||
->inject('events')
|
||||
->action(function (string $userId, string $number, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $number)
|
||||
->setAttribute('phoneVerification', false)
|
||||
;
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception('Email already exists', 409, Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
|
||||
$audits
|
||||
->setResource('user/' . $user->getId())
|
||||
;
|
||||
|
|
21
app/init.php
21
app/init.php
|
@ -23,11 +23,17 @@ use Ahc\Jwt\JWT;
|
|||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Phone\Mock;
|
||||
use Appwrite\Auth\Phone\Telesign;
|
||||
use Appwrite\Auth\Phone\TextMagic;
|
||||
use Appwrite\Auth\Phone\Twilio;
|
||||
use Appwrite\DSN\DSN;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Phone;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Network\Validator\IP;
|
||||
use Appwrite\Network\Validator\URL;
|
||||
|
@ -692,6 +698,7 @@ App::setResource('audits', fn() => new Audit());
|
|||
App::setResource('mails', fn() => new Mail());
|
||||
App::setResource('deletes', fn() => new Delete());
|
||||
App::setResource('database', fn() => new EventDatabase());
|
||||
App::setResource('messaging', fn() => new Phone());
|
||||
App::setResource('usage', function ($register) {
|
||||
return new Stats($register->get('statsd'));
|
||||
}, ['register']);
|
||||
|
@ -968,3 +975,17 @@ App::setResource('geodb', function ($register) {
|
|||
/** @var Utopia\Registry\Registry $register */
|
||||
return $register->get('geodb');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('phone', function () {
|
||||
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
|
||||
$user = $dsn->getUser();
|
||||
$secret = $dsn->getPassword();
|
||||
|
||||
return match ($dsn->getHost()) {
|
||||
'mock' => new Mock('', ''), // used for tests
|
||||
'twilio' => new Twilio($user, $secret),
|
||||
'text-magic' => new TextMagic($user, $secret),
|
||||
'telesign' => new Telesign($user, $secret),
|
||||
default => null
|
||||
};
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-name">
|
||||
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
|
||||
|
||||
<h2>Update name</h2>
|
||||
<h2>Update Name</h2>
|
||||
|
||||
<form name="users.updateName"
|
||||
data-analytics
|
||||
|
@ -85,6 +85,41 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-phone">
|
||||
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
|
||||
|
||||
<h2>Update Phone</h2>
|
||||
|
||||
<form name="users.updatePhone"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="console"
|
||||
data-analytics-label="Update User Phone"
|
||||
data-service="users.updatePhone"
|
||||
data-scope="sdk"
|
||||
data-event="submit"
|
||||
data-success="trigger,alert"
|
||||
data-success-param-alert-text="User phone was updated successfully"
|
||||
data-success-param-trigger-events="users.update"
|
||||
data-failure="alert"
|
||||
data-failure-param-alert-text="Failed to update user email"
|
||||
data-failure-param-alert-classname="error">
|
||||
|
||||
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
|
||||
|
||||
<label for="number">Phone number</label>
|
||||
<input name="number" id="number" type="text" autocomplete="off" data-ls-bind="{{user.phone}}" pattern="^\+?[1-9]\d{1,14}$" class="full-width" title="Phone number with a leading '+' and maximum of 15 digits (+123456789).">
|
||||
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Phone number with a leading '+' and maximum of 15 digits (+123456789)</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<footer>
|
||||
<button type="submit" class="">Update</button> <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-ui-modal class="modal width-medium box close" data-button-hide="on" data-open-event="open-update-password">
|
||||
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
|
||||
|
||||
|
@ -144,20 +179,31 @@
|
|||
<div class="col span-8">
|
||||
<label> </label>
|
||||
|
||||
<div class="box margin-bottom-large">
|
||||
<div class="box margin-bottom-large" style="padding-bottom: 5px">
|
||||
<div class="text-align-center">
|
||||
<img src="" data-ls-attrs="src={{user|avatar}}" data-size="200" alt="User Avatar" class="avatar huge margin-top-negative-xxl" />
|
||||
|
||||
<div class="margin-top-small" data-ls-bind="Member since {{user.registration|dateText}}"></div>
|
||||
<div class="margin-top-small">
|
||||
<span data-ls-if="{{user.emailVerification}} === true">
|
||||
<div class="margin-top-small margin-bottom-small" data-ls-bind="Member since {{user.registration|dateText}}"></div>
|
||||
<hr class="margin-top-tiny margin-bottom-tiny" data-ls-if="{{user.email}}">
|
||||
<div class="margin-top-small margin-bottom-small clear" data-ls-if="{{user.email}}">
|
||||
<span data-ls-bind="{{user.email}}" class="pull-start"></span>
|
||||
<span data-ls-if="{{user.emailVerification}} === true" class="pull-end">
|
||||
<span class="tag green">Verified</span>
|
||||
</span>
|
||||
<span data-ls-if="{{user.emailVerification}} !== true">
|
||||
<span data-ls-if="{{user.emailVerification}} !== true" class="pull-end">
|
||||
<span class="tag">Unverified</span>
|
||||
</span>
|
||||
</div>
|
||||
<hr class="margin-top-tiny margin-bottom-tiny" data-ls-if="{{user.phone}}">
|
||||
<div class="margin-top-small margin-bottom-small clear" data-ls-if="{{user.phone}}">
|
||||
<span data-ls-bind="{{user.phone}}" class="pull-start"></span>
|
||||
<span data-ls-if="{{user.phoneVerification}} === true" class="pull-end">
|
||||
<span class="tag green">Verified</span>
|
||||
</span>
|
||||
<span data-ls-if="{{user.phoneVerification}} !== true" class="pull-end">
|
||||
<span class="tag">Unverified</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="margin-top-small" data-ls-bind="{{user.email}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -236,20 +282,21 @@
|
|||
</div>
|
||||
|
||||
<ul class="margin-bottom-large text-fade text-size-small">
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-name" class="link text-size-small">Update name</button></li>
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-name" class="link text-size-small">Update Name</button></li>
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-email" class="link text-size-small">Update Email</button></li>
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-phone" class="link text-size-small">Update Phone</button></li>
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-update-password" class="link text-size-small">Update Password</button></li>
|
||||
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i> <button data-ls-ui-trigger="open-json" class="link text-size-small">View as JSON</button></li>
|
||||
</ul>
|
||||
|
||||
<div data-ls-if="{{user.emailVerification}} === false" style="display: none">
|
||||
<div data-ls-if="{{user.email}} && {{user.emailVerification}} === false" style="display: none">
|
||||
<form name="users.updateVerification" class="margin-bottom"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="console"
|
||||
data-analytics-label="Update Verification Status"
|
||||
data-service="users.updateVerification"
|
||||
data-analytics-label="Update Email Verification Status"
|
||||
data-service="users.updateEmailVerification"
|
||||
data-scope="sdk"
|
||||
data-event="submit"
|
||||
data-success="trigger,alert"
|
||||
|
@ -261,18 +308,18 @@
|
|||
>
|
||||
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
|
||||
<input type="hidden" disabled name="emailVerification" value="true" data-cast-to="boolean">
|
||||
<button type="submit" class="dark fill">Verify Account</button>
|
||||
<button type="submit" class="dark fill">Verify Email</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-ls-if="{{user.emailVerification}} === true" style="display: none">
|
||||
<div data-ls-if="{{user.email}} && {{user.emailVerification}} === true" style="display: none">
|
||||
<form name="users.updateVerification" class="margin-bottom"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="console"
|
||||
data-analytics-label="Update Verification Status"
|
||||
data-service="users.updateVerification"
|
||||
data-analytics-label="Update Email Verification Status"
|
||||
data-service="users.updateEmailVerification"
|
||||
data-scope="sdk"
|
||||
data-event="submit"
|
||||
data-success="trigger,alert"
|
||||
|
@ -284,7 +331,53 @@
|
|||
>
|
||||
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
|
||||
<input type="hidden" disabled name="emailVerification" value="false" data-cast-to="boolean">
|
||||
<button type="submit" class="dark fill">Unverify Account</button>
|
||||
<button type="submit" class="dark fill">Unverify Email</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-ls-if="{{user.phone}} && {{user.phoneVerification}} === false" style="display: none">
|
||||
<form name="users.updateVerification" class="margin-bottom"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="console"
|
||||
data-analytics-label="Update Phone Verification Status"
|
||||
data-service="users.updatePhoneVerification"
|
||||
data-scope="sdk"
|
||||
data-event="submit"
|
||||
data-success="trigger,alert"
|
||||
data-success-param-alert-text="User verification status was updated successfully"
|
||||
data-success-param-trigger-events="users.update"
|
||||
data-failure="alert"
|
||||
data-failure-param-alert-text="Failed to update user verification status"
|
||||
data-failure-param-alert-classname="error"
|
||||
>
|
||||
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
|
||||
<input type="hidden" disabled name="phoneVerification" value="true" data-cast-to="boolean">
|
||||
<button type="submit" class="dark fill">Verify Phone</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-ls-if="{{user.phone}} && {{user.phoneVerification}} === true" style="display: none">
|
||||
<form name="users.updateVerification" class="margin-bottom"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="console"
|
||||
data-analytics-label="Update Phone Verification Status"
|
||||
data-service="users.updatePhoneVerification"
|
||||
data-scope="sdk"
|
||||
data-event="submit"
|
||||
data-success="trigger,alert"
|
||||
data-success-param-alert-text="User verification status was updated successfully"
|
||||
data-success-param-trigger-events="users.update"
|
||||
data-failure="alert"
|
||||
data-failure-param-alert-text="Failed to update user verification status"
|
||||
data-failure-param-alert-classname="error"
|
||||
>
|
||||
<input type="hidden" disabled name="userId" data-ls-bind="{{user.$id}}">
|
||||
<input type="hidden" disabled name="phoneVerification" value="false" data-cast-to="boolean">
|
||||
<button type="submit" class="dark fill">Unverify Phone</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,17 +19,17 @@ $root = ($this->getParam('root') !== 'disabled');
|
|||
|
||||
<p>Login using email and password</p>
|
||||
|
||||
<form name="account.createSession"
|
||||
<form name="account.createEmailSession"
|
||||
data-analytics
|
||||
data-analytics-activity
|
||||
data-analytics-event="submit"
|
||||
data-analytics-category="home"
|
||||
data-analytics-label="Create Account Session"
|
||||
data-service="account.createSession"
|
||||
data-service="account.createEmailSession"
|
||||
data-scope="console"
|
||||
data-event="submit"
|
||||
data-success="trigger,hide,redirect"
|
||||
data-success-param-trigger-events="account.createSession"
|
||||
data-success-param-trigger-events="account.createEmailSession"
|
||||
data-success-param-redirect-url="/console"
|
||||
data-failure="alert"
|
||||
data-failure-param-alert-text="Login failed. Please check your credentials."
|
||||
|
|
63
app/workers/messaging.php
Normal file
63
app/workers/messaging.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
use Appwrite\Auth\Phone\Mock;
|
||||
use Appwrite\Auth\Phone\Telesign;
|
||||
use Appwrite\Auth\Phone\TextMagic;
|
||||
use Appwrite\Auth\Phone\Twilio;
|
||||
use Appwrite\DSN\DSN;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
Console::title('Messaging V1 Worker');
|
||||
Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
|
||||
|
||||
class MessagingV1 extends Worker
|
||||
{
|
||||
protected ?Phone $phone = null;
|
||||
protected ?string $from = null;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return "mails";
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
|
||||
$user = $dsn->getUser();
|
||||
$secret = $dsn->getPassword();
|
||||
|
||||
$this->phone = match ($dsn->getHost()) {
|
||||
'mock' => new Mock('', ''), // used for tests
|
||||
'twilio' => new Twilio($user, $secret),
|
||||
'text-magic' => new TextMagic($user, $secret),
|
||||
'telesign' => new Telesign($user, $secret),
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
|
||||
Console::info('Skipped sms processing. No Phone provider has been set.');
|
||||
return;
|
||||
}
|
||||
|
||||
$recipient = $this->args['recipient'];
|
||||
$message = $this->args['message'];
|
||||
|
||||
try {
|
||||
$this->phone->send($this->from, $recipient, $message);
|
||||
} catch (\Exception $error) {
|
||||
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
}
|
10
bin/worker-messaging
Normal file
10
bin/worker-messaging
Normal file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ]
|
||||
then
|
||||
REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
else
|
||||
REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}"
|
||||
fi
|
||||
|
||||
INTERVAL=1 QUEUE='v1-messaging' APP_INCLUDE='/usr/src/code/app/workers/messaging.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php
|
|
@ -175,6 +175,10 @@ services:
|
|||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_PHONE_PROVIDER
|
||||
- _APP_PHONE_FROM
|
||||
- _APP_PHONE_USER
|
||||
- _APP_PHONE_SECRET
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
|
@ -524,6 +528,32 @@ services:
|
|||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-messaging:
|
||||
entrypoint: worker-messaging
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-messaging
|
||||
build:
|
||||
context: .
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_PHONE_PROVIDER
|
||||
- _APP_PHONE_USER
|
||||
- _APP_PHONE_SECRET
|
||||
- _APP_PHONE_FROM
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-maintenance:
|
||||
entrypoint: maintenance
|
||||
<<: *x-logging
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](/docs/client/account#accountUpdateVerification). The verification link sent to the user's email address is valid for 7 days.
|
||||
Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](/docs/client/account#accountUpdateEmailVerification). The verification link sent to the user's email address is valid for 7 days.
|
||||
|
||||
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.
|
1
docs/references/account/create-phone-session.md
Normal file
1
docs/references/account/create-phone-session.md
Normal file
|
@ -0,0 +1 @@
|
|||
Sends the user a SMS with a secret key for creating a session. Use the returned user ID and the secret to submit a request to the [PUT /account/sessions/phone](/docs/client/account#accountUpdatePhoneSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.
|
1
docs/references/account/create-phone-verification.md
Normal file
1
docs/references/account/create-phone-verification.md
Normal file
|
@ -0,0 +1 @@
|
|||
Use this endpoint to send a verification message to your user's phone number to confirm they are the valid owners of that address. The provided secret should allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](/docs/client/account#accountUpdatePhoneVerification). The verification link sent to the user's phone number is valid for 15 minutes.
|
3
docs/references/account/update-phone-session.md
Normal file
3
docs/references/account/update-phone-session.md
Normal file
|
@ -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.
|
1
docs/references/account/update-phone-verification.md
Normal file
1
docs/references/account/update-phone-verification.md
Normal file
|
@ -0,0 +1 @@
|
|||
Use this endpoint to complete the user phone verification process. Use the **userId** and **secret** that were sent to your user's phone number to verify the user email ownership. If confirmed this route will return a 200 status code.
|
1
docs/references/account/update-phone.md
Normal file
1
docs/references/account/update-phone.md
Normal file
|
@ -0,0 +1 @@
|
|||
Update currently logged in user account phone number. After changing phone number, the user confirmation status will get reset. A new confirmation SMS is not sent automatically however you can use the phone confirmation endpoint again to send the confirmation SMS.
|
1
docs/references/users/update-user-phone-verification.md
Normal file
1
docs/references/users/update-user-phone-verification.md
Normal file
|
@ -0,0 +1 @@
|
|||
Update the user phone verification status by its unique ID.
|
1
docs/references/users/update-user-phone.md
Normal file
1
docs/references/users/update-user-phone.md
Normal file
|
@ -0,0 +1 @@
|
|||
Update the user phone by its unique ID.
|
34
public/dist/scripts/app-all.js
vendored
34
public/dist/scripts/app-all.js
vendored
|
@ -30,6 +30,10 @@ let path='/account/name';let payload={};if(typeof name!=='undefined'){payload['n
|
|||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(password,oldPassword)=>__awaiter(this,void 0,void 0,function*(){if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;}
|
||||
if(typeof oldPassword!=='undefined'){payload['oldPassword']=oldPassword;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhone:(number,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/phone';let payload={};if(typeof number!=='undefined'){payload['number']=number;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/prefs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
|
||||
let path='/account/prefs';let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createRecovery:(email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
|
@ -44,11 +48,11 @@ let path='/account/recovery';let payload={};if(typeof userId!=='undefined'){payl
|
|||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
if(typeof passwordAgain!=='undefined'){payload['passwordAgain']=passwordAgain;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createSession:(email,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createEmailSession:(email,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/sessions';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
let path='/account/sessions/email';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createMagicURLSession:(userId,email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createMagicURLSession:(userId,email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
let path='/account/sessions/magic-url';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof email!=='undefined'){payload['email']=email;}
|
||||
|
@ -63,7 +67,15 @@ if(typeof failure!=='undefined'){payload['failure']=failure;}
|
|||
if(typeof scopes!=='undefined'){payload['scopes']=scopes;}
|
||||
const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);}
|
||||
if(typeof window!=='undefined'&&(window===null||window===void 0?void 0:window.location)){window.location.href=uri.toString();}
|
||||
else{return uri;}},getSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
else{return uri;}},createPhoneSession:(userId,number)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
let path='/account/sessions/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof number!=='undefined'){payload['number']=number;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updatePhoneSession:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/sessions/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/status';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createVerification:(url)=>__awaiter(this,void 0,void 0,function*(){if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');}
|
||||
|
@ -72,6 +84,10 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{
|
|||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/verification';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),createPhoneVerification:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/verification/phone';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updatePhoneVerification:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/verification/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);})};this.avatars={getBrowser:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');}
|
||||
let path='/avatars/browsers/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;}
|
||||
if(typeof height!=='undefined'){payload['height']=height;}
|
||||
|
@ -616,6 +632,9 @@ let path='/users/{userId}/name'.replace('{userId}',userId);let payload={};if(typ
|
|||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(userId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/users/{userId}/password'.replace('{userId}',userId);let payload={};if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhone:(userId,number)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
let path='/users/{userId}/phone'.replace('{userId}',userId);let payload={};if(typeof number!=='undefined'){payload['number']=number;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
|
||||
|
@ -627,9 +646,12 @@ if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required
|
|||
let path='/users/{userId}/sessions/{sessionId}'.replace('{userId}',userId).replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:(userId,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');}
|
||||
let path='/users/{userId}/status'.replace('{userId}',userId);let payload={};if(typeof status!=='undefined'){payload['status']=status;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateVerification:(userId,emailVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateEmailVerification:(userId,emailVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof emailVerification==='undefined'){throw new AppwriteException('Missing required parameter: "emailVerification"');}
|
||||
let path='/users/{userId}/verification'.replace('{userId}',userId);let payload={};if(typeof emailVerification!=='undefined'){payload['emailVerification']=emailVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhoneVerification:(userId,phoneVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof phoneVerification==='undefined'){throw new AppwriteException('Missing required parameter: "phoneVerification"');}
|
||||
let path='/users/{userId}/verification/phone'.replace('{userId}',userId);let payload={};if(typeof phoneVerification!=='undefined'){payload['phoneVerification']=phoneVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;this.config.endpointRealtime=this.config.endpointRealtime||this.config.endpoint.replace('https://','wss://').replace('http://','ws://');return this;}
|
||||
setEndpointRealtime(endpointRealtime){this.config.endpointRealtime=endpointRealtime;return this;}
|
||||
|
@ -3570,7 +3592,7 @@ function handler2(){}
|
|||
handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);if(!root._x_refs)
|
||||
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
|
||||
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
|
||||
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{if(response.events.includes('stats.connections')){for(let project in response.payload){current[project]=response.payload[project]??0;}
|
||||
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createEmailSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{if(response.events.includes('stats.connections')){for(let project in response.payload){current[project]=response.payload[project]??0;}
|
||||
return;}
|
||||
if(response.events.includes('collections.*.attributes.*')){document.dispatchEvent(new CustomEvent('database.createAttribute'));return;}
|
||||
if(response.events.includes('collections.*.indexes.*')){document.dispatchEvent(new CustomEvent('database.createIndex'));return;}
|
||||
|
|
32
public/dist/scripts/app-dep.js
vendored
32
public/dist/scripts/app-dep.js
vendored
|
@ -30,6 +30,10 @@ let path='/account/name';let payload={};if(typeof name!=='undefined'){payload['n
|
|||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(password,oldPassword)=>__awaiter(this,void 0,void 0,function*(){if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;}
|
||||
if(typeof oldPassword!=='undefined'){payload['oldPassword']=oldPassword;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhone:(number,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/phone';let payload={};if(typeof number!=='undefined'){payload['number']=number;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/prefs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
|
||||
let path='/account/prefs';let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createRecovery:(email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
|
@ -44,11 +48,11 @@ let path='/account/recovery';let payload={};if(typeof userId!=='undefined'){payl
|
|||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
if(typeof passwordAgain!=='undefined'){payload['passwordAgain']=passwordAgain;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createSession:(email,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createEmailSession:(email,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account/sessions';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
let path='/account/sessions/email';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createMagicURLSession:(userId,email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createMagicURLSession:(userId,email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
let path='/account/sessions/magic-url';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof email!=='undefined'){payload['email']=email;}
|
||||
|
@ -63,7 +67,15 @@ if(typeof failure!=='undefined'){payload['failure']=failure;}
|
|||
if(typeof scopes!=='undefined'){payload['scopes']=scopes;}
|
||||
const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);}
|
||||
if(typeof window!=='undefined'&&(window===null||window===void 0?void 0:window.location)){window.location.href=uri.toString();}
|
||||
else{return uri;}},getSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
else{return uri;}},createPhoneSession:(userId,number)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
let path='/account/sessions/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof number!=='undefined'){payload['number']=number;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updatePhoneSession:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/sessions/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');}
|
||||
let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/status';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createVerification:(url)=>__awaiter(this,void 0,void 0,function*(){if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');}
|
||||
|
@ -72,6 +84,10 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{
|
|||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/verification';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),createPhoneVerification:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/verification/phone';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updatePhoneVerification:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');}
|
||||
let path='/account/verification/phone';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
|
||||
if(typeof secret!=='undefined'){payload['secret']=secret;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);})};this.avatars={getBrowser:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');}
|
||||
let path='/avatars/browsers/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;}
|
||||
if(typeof height!=='undefined'){payload['height']=height;}
|
||||
|
@ -616,6 +632,9 @@ let path='/users/{userId}/name'.replace('{userId}',userId);let payload={};if(typ
|
|||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(userId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/users/{userId}/password'.replace('{userId}',userId);let payload={};if(typeof password!=='undefined'){payload['password']=password;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhone:(userId,number)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof number==='undefined'){throw new AppwriteException('Missing required parameter: "number"');}
|
||||
let path='/users/{userId}/phone'.replace('{userId}',userId);let payload={};if(typeof number!=='undefined'){payload['number']=number;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');}
|
||||
|
@ -627,9 +646,12 @@ if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required
|
|||
let path='/users/{userId}/sessions/{sessionId}'.replace('{userId}',userId).replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:(userId,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');}
|
||||
let path='/users/{userId}/status'.replace('{userId}',userId);let payload={};if(typeof status!=='undefined'){payload['status']=status;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateVerification:(userId,emailVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateEmailVerification:(userId,emailVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof emailVerification==='undefined'){throw new AppwriteException('Missing required parameter: "emailVerification"');}
|
||||
let path='/users/{userId}/verification'.replace('{userId}',userId);let payload={};if(typeof emailVerification!=='undefined'){payload['emailVerification']=emailVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePhoneVerification:(userId,phoneVerification)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
|
||||
if(typeof phoneVerification==='undefined'){throw new AppwriteException('Missing required parameter: "phoneVerification"');}
|
||||
let path='/users/{userId}/verification/phone'.replace('{userId}',userId);let payload={};if(typeof phoneVerification!=='undefined'){payload['phoneVerification']=phoneVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;this.config.endpointRealtime=this.config.endpointRealtime||this.config.endpoint.replace('https://','wss://').replace('http://','ws://');return this;}
|
||||
setEndpointRealtime(endpointRealtime){this.config.endpointRealtime=endpointRealtime;return this;}
|
||||
|
|
2
public/dist/scripts/app.js
vendored
2
public/dist/scripts/app.js
vendored
|
@ -493,7 +493,7 @@ function handler2(){}
|
|||
handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);if(!root._x_refs)
|
||||
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
|
||||
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
|
||||
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{if(response.events.includes('stats.connections')){for(let project in response.payload){current[project]=response.payload[project]??0;}
|
||||
return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createEmailSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{if(response.events.includes('stats.connections')){for(let project in response.payload){current[project]=response.payload[project]??0;}
|
||||
return;}
|
||||
if(response.events.includes('collections.*.attributes.*')){document.dispatchEvent(new CustomEvent('database.createAttribute'));return;}
|
||||
if(response.events.includes('collections.*.indexes.*')){document.dispatchEvent(new CustomEvent('database.createIndex'));return;}
|
||||
|
|
|
@ -375,6 +375,35 @@
|
|||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Update Account Phone
|
||||
*
|
||||
*
|
||||
* @param {string} number
|
||||
* @param {string} password
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePhone: (number, password) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof number === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "number"');
|
||||
}
|
||||
if (typeof password === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "password"');
|
||||
}
|
||||
let path = '/account/phone';
|
||||
let payload = {};
|
||||
if (typeof number !== 'undefined') {
|
||||
payload['number'] = number;
|
||||
}
|
||||
if (typeof password !== 'undefined') {
|
||||
payload['password'] = password;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('patch', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Get Account Preferences
|
||||
*
|
||||
|
@ -522,37 +551,6 @@
|
|||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Account Session
|
||||
*
|
||||
* Allow the user to login into their account by providing a valid email and
|
||||
* password combination. This route will create a new session for the user.
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {string} password
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createSession: (email, password) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof email === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "email"');
|
||||
}
|
||||
if (typeof password === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "password"');
|
||||
}
|
||||
let path = '/account/sessions';
|
||||
let payload = {};
|
||||
if (typeof email !== 'undefined') {
|
||||
payload['email'] = email;
|
||||
}
|
||||
if (typeof password !== 'undefined') {
|
||||
payload['password'] = password;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('post', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Delete All Account Sessions
|
||||
*
|
||||
|
@ -591,6 +589,37 @@
|
|||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Account Session with Email
|
||||
*
|
||||
* Allow the user to login into their account by providing a valid email and
|
||||
* password combination. This route will create a new session for the user.
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {string} password
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createEmailSession: (email, password) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof email === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "email"');
|
||||
}
|
||||
if (typeof password === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "password"');
|
||||
}
|
||||
let path = '/account/sessions/email';
|
||||
let payload = {};
|
||||
if (typeof email !== 'undefined') {
|
||||
payload['email'] = email;
|
||||
}
|
||||
if (typeof password !== 'undefined') {
|
||||
payload['password'] = password;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('post', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Magic URL session
|
||||
*
|
||||
|
@ -724,6 +753,64 @@
|
|||
return uri;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create Phone session
|
||||
*
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} number
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createPhoneSession: (userId, number) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
if (typeof number === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "number"');
|
||||
}
|
||||
let path = '/account/sessions/phone';
|
||||
let payload = {};
|
||||
if (typeof userId !== 'undefined') {
|
||||
payload['userId'] = userId;
|
||||
}
|
||||
if (typeof number !== 'undefined') {
|
||||
payload['number'] = number;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('post', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Phone session (confirmation)
|
||||
*
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} secret
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePhoneSession: (userId, secret) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
if (typeof secret === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "secret"');
|
||||
}
|
||||
let path = '/account/sessions/phone';
|
||||
let payload = {};
|
||||
if (typeof userId !== 'undefined') {
|
||||
payload['userId'] = userId;
|
||||
}
|
||||
if (typeof secret !== 'undefined') {
|
||||
payload['secret'] = secret;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('put', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Get Session By ID
|
||||
*
|
||||
|
@ -877,6 +964,50 @@
|
|||
return yield this.call('put', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Phone Verification
|
||||
*
|
||||
*
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createPhoneVerification: () => __awaiter(this, void 0, void 0, function* () {
|
||||
let path = '/account/verification/phone';
|
||||
let payload = {};
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('post', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Create Phone Verification (confirmation)
|
||||
*
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} secret
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePhoneVerification: (userId, secret) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
if (typeof secret === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "secret"');
|
||||
}
|
||||
let path = '/account/verification/phone';
|
||||
let payload = {};
|
||||
if (typeof userId !== 'undefined') {
|
||||
payload['userId'] = userId;
|
||||
}
|
||||
if (typeof secret !== 'undefined') {
|
||||
payload['secret'] = secret;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('put', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
})
|
||||
};
|
||||
this.avatars = {
|
||||
|
@ -5314,6 +5445,32 @@
|
|||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Update Phone
|
||||
*
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} number
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePhone: (userId, number) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
if (typeof number === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "number"');
|
||||
}
|
||||
let path = '/users/{userId}/phone'.replace('{userId}', userId);
|
||||
let payload = {};
|
||||
if (typeof number !== 'undefined') {
|
||||
payload['number'] = number;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('patch', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Get User Preferences
|
||||
*
|
||||
|
@ -5465,7 +5622,7 @@
|
|||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateVerification: (userId, emailVerification) => __awaiter(this, void 0, void 0, function* () {
|
||||
updateEmailVerification: (userId, emailVerification) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
|
@ -5481,6 +5638,32 @@
|
|||
return yield this.call('patch', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Update Phone Verification
|
||||
*
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {boolean} phoneVerification
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePhoneVerification: (userId, phoneVerification) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof userId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "userId"');
|
||||
}
|
||||
if (typeof phoneVerification === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "phoneVerification"');
|
||||
}
|
||||
let path = '/users/{userId}/verification/phone'.replace('{userId}', userId);
|
||||
let payload = {};
|
||||
if (typeof phoneVerification !== 'undefined') {
|
||||
payload['phoneVerification'] = phoneVerification;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('patch', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ document.addEventListener("account.create", function () {
|
|||
let form = container.get('serviceForm');
|
||||
let sdk = container.get('console');
|
||||
|
||||
let promise = sdk.account.createSession(form.email, form.password);
|
||||
let promise = sdk.account.createEmailSession(form.email, form.password);
|
||||
|
||||
container.set("serviceForm", {}, true, true); // Remove sensitive data when not needed
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class Auth
|
|||
public const TOKEN_TYPE_RECOVERY = 3;
|
||||
public const TOKEN_TYPE_INVITE = 4;
|
||||
public const TOKEN_TYPE_MAGIC_URL = 5;
|
||||
public const TOKEN_TYPE_PHONE = 6;
|
||||
|
||||
/**
|
||||
* Session Providers.
|
||||
|
@ -34,6 +35,7 @@ class Auth
|
|||
public const SESSION_PROVIDER_EMAIL = 'email';
|
||||
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
|
||||
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
|
||||
public const SESSION_PROVIDER_PHONE = 'phone';
|
||||
|
||||
/**
|
||||
* Token Expiration times.
|
||||
|
@ -42,6 +44,7 @@ class Auth
|
|||
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
|
||||
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
|
||||
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 24 * 7; /* 7 days */
|
||||
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
@ -195,7 +198,8 @@ class Auth
|
|||
*/
|
||||
public static function tokenVerify(array $tokens, int $type, string $secret)
|
||||
{
|
||||
foreach ($tokens as $token) { /** @var Document $token */
|
||||
foreach ($tokens as $token) {
|
||||
/** @var Document $token */
|
||||
if (
|
||||
$token->isSet('type') &&
|
||||
$token->isSet('secret') &&
|
||||
|
@ -211,6 +215,25 @@ class Auth
|
|||
return false;
|
||||
}
|
||||
|
||||
public static function phoneTokenVerify(array $tokens, string $secret)
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
/** @var Document $token */
|
||||
if (
|
||||
$token->isSet('type') &&
|
||||
$token->isSet('secret') &&
|
||||
$token->isSet('expire') &&
|
||||
$token->getAttribute('type') == Auth::TOKEN_TYPE_PHONE &&
|
||||
$token->getAttribute('secret') === $secret &&
|
||||
$token->getAttribute('expire') >= \time()
|
||||
) {
|
||||
return (string) $token->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
|
@ -221,7 +244,8 @@ class Auth
|
|||
*/
|
||||
public static function sessionVerify(array $sessions, string $secret)
|
||||
{
|
||||
foreach ($sessions as $session) { /** @var Document $session */
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
if (
|
||||
$session->isSet('secret') &&
|
||||
$session->isSet('expire') &&
|
||||
|
@ -303,4 +327,11 @@ class Auth
|
|||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public static function isAnonymousUser(Document $user): bool
|
||||
{
|
||||
return (is_null($user->getAttribute('email'))
|
||||
|| is_null($user->getAttribute('phone'))
|
||||
) && is_null($user->getAttribute('password'));
|
||||
}
|
||||
}
|
||||
|
|
88
src/Appwrite/Auth/Phone.php
Normal file
88
src/Appwrite/Auth/Phone.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
|
||||
abstract class Phone
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $secret;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function __construct(string $user, string $secret)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Message to phone.
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
abstract public function send(string $from, string $to, string $message): void;
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $url
|
||||
* @param array $headers
|
||||
* @param string $payload
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function request(string $method, string $url, array $headers = [], ?string $payload = null, ?string $userpwd = null): string
|
||||
{
|
||||
$ch = \curl_init($url);
|
||||
|
||||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
\curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
\curl_setopt($ch, CURLOPT_USERAGENT, 'Appwrite Phone Authentication');
|
||||
|
||||
if (!is_null($payload)) {
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
}
|
||||
|
||||
if (!is_null($userpwd)) {
|
||||
\curl_setopt($ch, CURLOPT_USERPWD, $userpwd);
|
||||
}
|
||||
|
||||
$headers[] = 'Content-length: ' . \strlen($payload);
|
||||
|
||||
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$response = (string) \curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($code >= 400) {
|
||||
throw new Exception($response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate 6 random digits for phone verification.
|
||||
*
|
||||
* @param int $digits
|
||||
* @return string
|
||||
*/
|
||||
public function generateSecretDigits(int $digits = 6): string
|
||||
{
|
||||
return substr(str_shuffle("0123456789"), 0, $digits);
|
||||
}
|
||||
}
|
33
src/Appwrite/Auth/Phone/Mock.php
Normal file
33
src/Appwrite/Auth/Phone/Mock.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
class Mock extends Phone
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static string $defaultDigits = '123456';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $digits
|
||||
* @return string
|
||||
*/
|
||||
public function generateSecretDigits(int $digits = 6): string
|
||||
{
|
||||
return self::$defaultDigits;
|
||||
}
|
||||
}
|
39
src/Appwrite/Auth/Phone/Telesign.php
Normal file
39
src/Appwrite/Auth/Phone/Telesign.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://www.twilio.com/docs/sms/api
|
||||
|
||||
class Telesign extends Phone
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://rest-api.telesign.com/v1/messaging';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
* @throws \Appwrite\Extend\Exception
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint,
|
||||
payload: \http_build_query([
|
||||
'message' => $message,
|
||||
'message_type' => 'otp',
|
||||
'phone_number' => $to
|
||||
]),
|
||||
userpwd: "{$this->user}:{$this->secret}"
|
||||
);
|
||||
}
|
||||
}
|
42
src/Appwrite/Auth/Phone/TextMagic.php
Normal file
42
src/Appwrite/Auth/Phone/TextMagic.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://www.textmagic.com/docs/api/start/
|
||||
|
||||
class TextMagic extends Phone
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://rest.textmagic.com/api/v2';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$to = ltrim($to, '+');
|
||||
$from = ltrim($from, '+');
|
||||
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: $this->endpoint . '/messages',
|
||||
payload: \http_build_query([
|
||||
'text' => $message,
|
||||
'from' => $from,
|
||||
'phones' => $to
|
||||
]),
|
||||
headers: [
|
||||
"X-TM-Username: {$this->user}",
|
||||
"X-TM-Key: {$this->secret}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
36
src/Appwrite/Auth/Phone/Twilio.php
Normal file
36
src/Appwrite/Auth/Phone/Twilio.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Phone;
|
||||
|
||||
use Appwrite\Auth\Phone;
|
||||
|
||||
// Reference Material
|
||||
// https://www.twilio.com/docs/sms/api
|
||||
|
||||
class Twilio extends Phone
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $endpoint = 'https://api.twilio.com/2010-04-01';
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param string $message
|
||||
* @return void
|
||||
*/
|
||||
public function send(string $from, string $to, string $message): void
|
||||
{
|
||||
$this->request(
|
||||
method: 'POST',
|
||||
url: "{$this->endpoint}/Accounts/{$this->user}/Messages.json",
|
||||
payload: \http_build_query([
|
||||
'Body' => $message,
|
||||
'From' => $from,
|
||||
'To' => $to
|
||||
]),
|
||||
userpwd: "{$this->user}:{$this->secret}"
|
||||
);
|
||||
}
|
||||
}
|
61
src/Appwrite/Auth/Validator/Phone.php
Normal file
61
src/Appwrite/Auth/Validator/Phone.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* Phone.
|
||||
*
|
||||
* Validates a number for the E.164 format.
|
||||
*/
|
||||
class Phone extends Validator
|
||||
{
|
||||
/**
|
||||
* Get Description.
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Phone number must start with a '+' can have a maximum of fifteen digits.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
return is_string($value) && !!\preg_match('/^\+[1-9]\d{1,14}$/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
|
@ -32,6 +32,9 @@ class Event
|
|||
public const BUILDS_QUEUE_NAME = 'v1-builds';
|
||||
public const BUILDS_CLASS_NAME = 'BuildsV1';
|
||||
|
||||
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
|
||||
public const MESSAGING_CLASS_NAME = 'MessagingV1';
|
||||
|
||||
protected string $queue = '';
|
||||
protected string $class = '';
|
||||
protected string $event = '';
|
||||
|
|
80
src/Appwrite/Event/Phone.php
Normal file
80
src/Appwrite/Event/Phone.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Resque;
|
||||
|
||||
class Phone extends Event
|
||||
{
|
||||
protected string $recipient = '';
|
||||
protected string $message = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Event::MESSAGING_QUEUE_NAME, Event::MESSAGING_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets recipient for the messaging event.
|
||||
*
|
||||
* @param string $recipient
|
||||
* @return self
|
||||
*/
|
||||
public function setRecipient(string $recipient): self
|
||||
{
|
||||
$this->recipient = $recipient;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set recipient for this messaging event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRecipient(): string
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets url for the messaging event.
|
||||
*
|
||||
* @param string $message
|
||||
* @return self
|
||||
*/
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set url for the messaging event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the event and sends it to the messaging worker.
|
||||
*
|
||||
* @return string|bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
return Resque::enqueue($this->queue, $this->class, [
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'payload' => $this->payload,
|
||||
'recipient' => $this->recipient,
|
||||
'message' => $this->message,
|
||||
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -59,12 +59,24 @@ class User extends Model
|
|||
'default' => '',
|
||||
'example' => 'john@appwrite.io',
|
||||
])
|
||||
->addRule('phone', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User phone number in E.164 format.',
|
||||
'default' => '',
|
||||
'example' => '+4930901820',
|
||||
])
|
||||
->addRule('emailVerification', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Email verification status.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('phoneVerification', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Phone verification status.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('prefs', [
|
||||
'type' => Response::MODEL_PREFERENCES,
|
||||
'description' => 'User preferences as a key-value object',
|
||||
|
|
|
@ -95,7 +95,7 @@ abstract class Scope extends TestCase
|
|||
|
||||
$this->assertEquals(201, $root['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => 'console',
|
||||
|
@ -147,7 +147,7 @@ abstract class Scope extends TestCase
|
|||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
|
|
@ -106,7 +106,7 @@ trait AccountBase
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -123,7 +123,7 @@ trait AccountBase
|
|||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -134,7 +134,7 @@ trait AccountBase
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 401);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -145,7 +145,7 @@ trait AccountBase
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 401);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -483,8 +483,7 @@ trait AccountBase
|
|||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
]);
|
||||
]), []);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
|
@ -533,7 +532,7 @@ trait AccountBase
|
|||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -559,8 +558,7 @@ trait AccountBase
|
|||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
]);
|
||||
]), []);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
|
@ -640,8 +638,7 @@ trait AccountBase
|
|||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
]);
|
||||
]), []);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
|
@ -924,7 +921,7 @@ trait AccountBase
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -991,7 +988,7 @@ trait AccountBase
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -1073,7 +1070,7 @@ trait AccountBase
|
|||
$email = $data['email'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -1432,7 +1429,7 @@ trait AccountBase
|
|||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -1459,8 +1456,7 @@ trait AccountBase
|
|||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
]);
|
||||
]), []);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\E2E\Services\Account;
|
||||
|
||||
use Appwrite\Auth\Phone\Mock;
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
|
@ -79,7 +80,7 @@ class AccountCustomClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -121,7 +122,7 @@ class AccountCustomClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 401);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -160,7 +161,7 @@ class AccountCustomClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -201,7 +202,7 @@ class AccountCustomClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 401);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -239,7 +240,7 @@ class AccountCustomClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -448,7 +449,7 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -674,4 +675,312 @@ class AccountCustomClientTest extends Scope
|
|||
$this->assertCount(1, $response['body']['users']);
|
||||
$this->assertEquals($response['body']['users'][0]['email'], $email);
|
||||
}
|
||||
|
||||
|
||||
public function testCreatePhone(): array
|
||||
{
|
||||
$number = '+123456789';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => 'unique()',
|
||||
'number' => $number,
|
||||
]);
|
||||
|
||||
$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'];
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => 'unique()'
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$data['token'] = Mock::$defaultDigits;
|
||||
$data['id'] = $userId;
|
||||
$data['number'] = $number;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreatePhone
|
||||
*/
|
||||
public function testCreateSessionWithPhone(array $data): array
|
||||
{
|
||||
$id = $data['id'] ?? '';
|
||||
$token = $data['token'] ?? '';
|
||||
$number = $data['number'] ?? '';
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', 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/phone', 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']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', 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']);
|
||||
|
||||
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['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($response['headers']['status-code'], 200);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['phone'], $number);
|
||||
$this->assertTrue($response['body']['phoneVerification']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => $id,
|
||||
'secret' => $token,
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
$data['session'] = $session;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateSessionWithPhone
|
||||
*/
|
||||
public function testConvertPhoneToPassword(array $data): array
|
||||
{
|
||||
$session = $data['session'];
|
||||
$email = uniqid() . 'new@localhost.test';
|
||||
$password = 'new-password';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$email = uniqid() . 'new@localhost.test';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 200);
|
||||
$this->assertIsArray($response['body']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testConvertPhoneToPassword
|
||||
*/
|
||||
public function testUpdatePhone(array $data): array
|
||||
{
|
||||
$newPhone = '+45632569856';
|
||||
$session = $data['session'] ?? '';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'number' => $newPhone,
|
||||
'password' => 'new-password'
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 200);
|
||||
$this->assertIsArray($response['body']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertIsNumeric($response['body']['registration']);
|
||||
$this->assertEquals($response['body']['phone'], $newPhone);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]));
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 401);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), []);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
$data['phone'] = $newPhone;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testUpdatePhone
|
||||
*/
|
||||
public function testPhoneVerification(array $data): array
|
||||
{
|
||||
$session = $data['session'] ?? '';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/verification/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
|
||||
]));
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertIsNumeric($response['body']['expire']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testPhoneVerification
|
||||
*/
|
||||
public function testUpdatePhoneVerification($data): array
|
||||
{
|
||||
$id = $data['id'] ?? '';
|
||||
$session = $data['session'] ?? '';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'userId' => $id,
|
||||
'secret' => Mock::$defaultDigits,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'userId' => 'ewewe',
|
||||
'secret' => Mock::$defaultDigits,
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/verification/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]), [
|
||||
'userId' => $id,
|
||||
'secret' => '999999',
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ trait DatabasePermissionsScope
|
|||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
|
|
@ -425,7 +425,7 @@ class ProjectsConsoleClientTest extends Scope
|
|||
'name' => $originalName,
|
||||
]);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
|
@ -514,7 +514,7 @@ class ProjectsConsoleClientTest extends Scope
|
|||
|
||||
$this->assertEquals($response['headers']['status-code'], 501);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
|
|
|
@ -458,7 +458,7 @@ class RealtimeCustomClientTest extends Scope
|
|||
/**
|
||||
* Test Account Session Create
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
|
|
|
@ -404,7 +404,7 @@ trait UsersBase
|
|||
$this->assertEquals($user['headers']['status-code'], 200);
|
||||
$this->assertNotEmpty($user['body']['$id']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
|
|
|
@ -91,7 +91,7 @@ class WebhooksCustomClientTest extends Scope
|
|||
'name' => $name,
|
||||
]);
|
||||
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -154,7 +154,7 @@ class WebhooksCustomClientTest extends Scope
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -234,7 +234,7 @@ class WebhooksCustomClientTest extends Scope
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -320,7 +320,7 @@ class WebhooksCustomClientTest extends Scope
|
|||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -391,7 +391,7 @@ class WebhooksCustomClientTest extends Scope
|
|||
$this->assertIsString($webhook['data']['countryName']);
|
||||
$this->assertEquals($webhook['data']['current'], true);
|
||||
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
|
||||
$accountSession = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
|
42
tests/unit/Auth/Validator/PhoneTest.php
Normal file
42
tests/unit/Auth/Validator/PhoneTest.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PhoneTest extends TestCase
|
||||
{
|
||||
protected ?Phone $object = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->object = new Phone();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function testValues()
|
||||
{
|
||||
$this->assertEquals($this->object->isValid(false), false);
|
||||
$this->assertEquals($this->object->isValid(null), false);
|
||||
$this->assertEquals($this->object->isValid(''), false);
|
||||
$this->assertEquals($this->object->isValid('+1'), false);
|
||||
$this->assertEquals($this->object->isValid('8989829304'), false);
|
||||
$this->assertEquals($this->object->isValid('786-307-3615'), false);
|
||||
$this->assertEquals($this->object->isValid('+16308A520397'), false);
|
||||
$this->assertEquals($this->object->isValid('+0415553452342'), false);
|
||||
$this->assertEquals($this->object->isValid('+14 155 5524564'), false);
|
||||
$this->assertEquals($this->object->isValid(+14155552456), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('+14155552'), true);
|
||||
$this->assertEquals($this->object->isValid('+141555526'), true);
|
||||
$this->assertEquals($this->object->isValid('+16308520394'), true);
|
||||
$this->assertEquals($this->object->isValid('+163085205339'), true);
|
||||
$this->assertEquals($this->object->isValid('+5511552563253'), true);
|
||||
$this->assertEquals($this->object->isValid('+55115525632534'), true);
|
||||
$this->assertEquals($this->object->isValid('+919367788755111'), true);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue