1
0
Fork 0
mirror of synced 2024-07-01 04:30:59 +12:00

Merge remote-tracking branch 'origin/1.5.x' into feat-message-scheduling

# Conflicts:
#	app/controllers/api/messaging.php
This commit is contained in:
Jake Barnby 2024-01-19 15:54:21 +13:00
commit a3096bc209
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
44 changed files with 2550 additions and 2011 deletions

View file

@ -7,21 +7,21 @@ return [
'name' => 'Email/Password',
'key' => 'emailPassword',
'icon' => '/images/users/email.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateEmailSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateEmailPasswordSession',
'enabled' => true,
],
'magic-url' => [
'name' => 'Magic URL',
'key' => 'usersAuthMagicURL',
'icon' => '/images/users/magic-url.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateMagicURLSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateMagicURLToken',
'enabled' => true,
],
'anonymous' => [
'name' => 'Anonymous',
'key' => 'anonymous',
'icon' => '/images/users/anonymous.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateAnonymousSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreateAnonymousSession',
'enabled' => true,
],
'invites' => [
@ -42,7 +42,7 @@ return [
'name' => 'Phone',
'key' => 'phone',
'icon' => '/images/users/phone.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreatePhoneSession',
'docs' => 'https://appwrite.io/docs/references/cloud/client-web/account#accountCreatePhoneToken',
'enabled' => true,
],
];

View file

@ -58,6 +58,14 @@ return [
'$description' => 'This event triggers when a user\'s target is deleted.',
],
],
'tokens' => [
'$model' => Response::MODEL_TOKEN,
'$resource' => true,
'$description' => 'This event triggers on any user\'s token event.',
'create' => [
'$description' => 'This event triggers when a user\'s token is created.',
],
],
'create' => [
'$description' => 'This event triggers when a user is created.'
],

View file

@ -8,7 +8,9 @@ $member = [
'home',
'console',
'graphql',
'account',
'sessions.write',
'accounts.read',
'accounts.write',
'teams.read',
'teams.write',
'documents.read',
@ -31,6 +33,7 @@ $member = [
$admins = [
'global',
'graphql',
'sessions.write',
'teams.read',
'teams.write',
'documents.read',
@ -85,6 +88,7 @@ return [
'home',
'console',
'graphql',
'sessions.write',
'documents.read',
'documents.write',
'files.read',

View file

@ -1,6 +1,15 @@
<?php
return [ // List of publicly visible scopes
'accounts.read' => [
'description' => 'Access to read your active user account',
],
'accounts.write' => [
'description' => 'Access to create, update, and delete your active user account',
],
'sessions.write' => [
'description' => 'Access to create, update, and delete user sessions',
],
'users.read' => [
'description' => 'Access to read your project\'s users',
],

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

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Enum\MessageStatus;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Messaging;
@ -1685,12 +1686,9 @@ App::post('/v1/messaging/topics')
$topic = new Document([
'$id' => $topicId,
'name' => $name,
'description' => $description
]);
if ($description) {
$topic->setAttribute('description', $description);
}
try {
$topic = $dbForProject->createDocument('topics', $topic);
} catch (DuplicateException) {
@ -2271,7 +2269,7 @@ App::post('/v1/messaging/messages/email')
->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
->param('description', '', new Text(256), 'Description for message.', true)
->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
@ -2285,7 +2283,7 @@ App::post('/v1/messaging/messages/email')
? ID::unique()
: $messageId;
if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
@ -2327,7 +2325,7 @@ App::post('/v1/messaging/messages/email')
'status' => $status,
]));
if ($status === 'processing' && \is_null($scheduledAt)) {
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setMessageId($message->getId())
->trigger();
@ -2381,7 +2379,7 @@ App::post('/v1/messaging/messages/sms')
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('description', '', new Text(256), 'Description for Message.', true)
->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
@ -2394,23 +2392,25 @@ App::post('/v1/messaging/messages/sms')
? ID::unique()
: $messageId;
if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
Query::equal('providerType', [MESSAGE_TYPE_SMS]),
Query::limit(\count($targets)),
]);
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
Query::equal('providerType', [MESSAGE_TYPE_SMS]),
Query::limit(\count($targets)),
]);
if (\count($foundTargets) !== \count($targets)) {
throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS);
}
if (\count($foundTargets) !== \count($targets)) {
throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS);
}
foreach ($foundTargets as $target) {
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
foreach ($foundTargets as $target) {
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
}
}
@ -2427,7 +2427,7 @@ App::post('/v1/messaging/messages/sms')
'status' => $status,
]));
if ($status === 'processing' && $scheduledAt === null) {
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setMessageId($message->getId())
->trigger();
@ -2484,7 +2484,7 @@ App::post('/v1/messaging/messages/push')
->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true)
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true)
->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
@ -2497,23 +2497,25 @@ App::post('/v1/messaging/messages/push')
? ID::unique()
: $messageId;
if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
Query::equal('providerType', [MESSAGE_TYPE_PUSH]),
Query::limit(\count($targets)),
]);
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
Query::equal('providerType', [MESSAGE_TYPE_PUSH]),
Query::limit(\count($targets)),
]);
if (\count($foundTargets) !== \count($targets)) {
throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH);
}
if (\count($foundTargets) !== \count($targets)) {
throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH);
}
foreach ($foundTargets as $target) {
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
foreach ($foundTargets as $target) {
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
}
}
@ -2539,7 +2541,7 @@ App::post('/v1/messaging/messages/push')
'status' => $status,
]));
if ($status === 'processing' && $scheduledAt === null) {
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setMessageId($message->getId())
->trigger();
@ -2740,7 +2742,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
->param('subject', null, new Text(998), 'Email Subject.', true)
->param('description', null, new Text(256), 'Description for Message.', true)
->param('content', null, new Text(64230), 'Email Content.', true)
->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true)
->param('html', null, new Boolean(), 'Is content of type HTML', true)
->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true)
->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true)
@ -2758,7 +2760,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
throw new Exception(Exception::MESSAGE_NOT_FOUND);
}
if ($message->getAttribute('status') === 'sent') {
if ($message->getAttribute('status') === MessageStatus::SENT) {
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
}
@ -2845,7 +2847,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === 'processing' && \is_null($message->getAttribute('scheduledAt'))) {
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setMessageId($message->getId())
->trigger();

View file

@ -474,6 +474,7 @@ App::post('/v1/teams/:teamId/memberships')
'phone' => empty($phone) ? null : $phone,
'emailVerification' => false,
'status' => true,
// TODO: Set password empty?
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,

View file

@ -14,6 +14,7 @@ use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
@ -35,6 +36,7 @@ use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
use Appwrite\Auth\Validator\PasswordHistory;
@ -1420,6 +1422,134 @@ App::patch('/v1/users/:userId/targets/:targetId')
->dynamic($target, Response::MODEL_TARGET);
});
App::post('/v1/users/:userId/sessions')
->desc('Create session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'users.write')
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSession')
->label('sdk.description', '/docs/references/users/create-session.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::codeGenerator();
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session = $dbForProject->createDocument('sessions', $session);
$session
->setAttribute('secret', $secret)
->setAttribute('expire', $expire)
->setAttribute('countryName', $countryName);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/users/:userId/tokens')
->desc('Create token')
->groups(['api', 'users'])
->label('event', 'users.[userId].tokens.[tokenId].create')
->label('scope', 'users.write')
->label('audits.event', 'tokens.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'tokens.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createToken')
->label('sdk.description', '/docs/references/users/create-token.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator($length);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP()
]);
$token = $dbForProject->createDocument('tokens', $token);
$dbForProject->deleteCachedDocument('users', $user->getId());
$token->setAttribute('secret', $secret);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])

View file

@ -425,8 +425,8 @@ App::init()
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true');
@ -589,8 +589,8 @@ App::options()
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();

View file

@ -316,6 +316,12 @@ App::init()
}
break;
case 'phone':
if (($auths['phone'] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
}
break;
default:
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
break;

View file

@ -55,6 +55,12 @@ App::init()
}
break;
case 'phone':
if (($auths['phone'] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
}
break;
default:
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
break;

View file

@ -1112,9 +1112,18 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
Auth::$cookieName, // Get sessions
$request->getCookie(Auth::$cookieName . '_legacy', '')
)
);// Get fallback session from old clients (no SameSite support)
);
// Get fallback session from clients who block 3rd-party cookies
// Get session from header for SSR clients
if (empty($session['id']) && empty($session['secret'])) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$session = Auth::decodeSession($sessionHeader);
}
}
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
if ($response) {
$response->addHeader('X-Debug-Fallback', 'false');
}

View file

@ -0,0 +1 @@
Use this endpoint to create a session from token. Provide the **userId** and **secret** parameters from the successful response of authentication flows initiated by token creation. For example, magic URL and phone login.

View file

@ -1,3 +0,0 @@
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](https://appwrite.io/docs/references/cloud/client-web/account#createMagicURLSession) 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.

View file

@ -1 +0,0 @@
Use this endpoint to complete creating a session with SMS. Use the **userId** from the [createPhoneSession](https://appwrite.io/docs/references/cloud/client-web/account#createPhoneSession) endpoint and the **secret** received via SMS to successfully update and confirm the phone session.

View file

@ -0,0 +1,3 @@
Creates a session for a user. Returns an immediately usable session object.
If you want to generate a token for a custom authentication flow, use the [POST /users/{userId}/tokens](https://appwrite.io/docs/server/users#createToken) endpoint.

View file

@ -0,0 +1 @@
Returns a token with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the [PUT /account/sessions/custom](https://appwrite.io/docs/references/cloud/client-web/account#updateCustomSession) endpoint to complete the login process.

View file

@ -52,6 +52,8 @@ class Auth
public const TOKEN_TYPE_INVITE = 4;
public const TOKEN_TYPE_MAGIC_URL = 5;
public const TOKEN_TYPE_PHONE = 6;
public const TOKEN_TYPE_OAUTH2 = 7;
public const TOKEN_TYPE_GENERIC = 8;
/**
* Session Providers.
@ -60,6 +62,9 @@ class Auth
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
public const SESSION_PROVIDER_PHONE = 'phone';
public const SESSION_PROVIDER_OAUTH2 = 'oauth2';
public const SESSION_PROVIDER_TOKEN = 'token';
public const SESSION_PROVIDER_SERVER = 'server';
/**
* Token Expiration times.
@ -69,6 +74,16 @@ class Auth
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
public const TOKEN_LENGTH_MAGIC_URL = 64;
public const TOKEN_LENGTH_VERIFICATION = 256;
public const TOKEN_LENGTH_RECOVERY = 256;
public const TOKEN_LENGTH_OAUTH2 = 64;
public const TOKEN_LENGTH_SESSION = 256;
/**
* @var string
@ -117,6 +132,27 @@ class Auth
]));
}
/**
* Token type to session provider mapping.
*/
public static function getSessionProviderByTokenType(int $type): string
{
switch ($type) {
case Auth::TOKEN_TYPE_VERIFICATION:
case Auth::TOKEN_TYPE_RECOVERY:
case Auth::TOKEN_TYPE_INVITE:
return Auth::SESSION_PROVIDER_EMAIL;
case Auth::TOKEN_TYPE_MAGIC_URL:
return Auth::SESSION_PROVIDER_MAGIC_URL;
case Auth::TOKEN_TYPE_PHONE:
return Auth::SESSION_PROVIDER_PHONE;
case Auth::TOKEN_TYPE_OAUTH2:
return Auth::SESSION_PROVIDER_OAUTH2;
default:
return Auth::SESSION_PROVIDER_TOKEN;
}
}
/**
* Decode Session.
*
@ -270,13 +306,20 @@ class Auth
*
* Generate random password string
*
* @param int $length
* @param int $length Length of returned token
*
* @return string
*/
public static function tokenGenerator(int $length = 128): string
public static function tokenGenerator(int $length = 256): string
{
return \bin2hex(\random_bytes($length));
if ($length <= 0) {
throw new \Exception('Token length must be greater than 0');
}
$bytesLength = (int) ceil($length / 2);
$token = \bin2hex(\random_bytes($bytesLength));
return substr($token, 0, $length);
}
/**
@ -303,43 +346,24 @@ class Auth
* Verify token and check that its not expired.
*
* @param array $tokens
* @param int $type
* @param int $type Type of token to verify, if null will verify any type
* @param string $secret
*
* @return bool|string
* @return false|Document
*/
public static function tokenVerify(array $tokens, int $type, string $secret)
public static function tokenVerify(array $tokens, int $type = null, string $secret): false|Document
{
foreach ($tokens as $token) {
/** @var Document $token */
if (
$token->isSet('type') &&
$token->isSet('secret') &&
$token->isSet('expire') &&
$token->getAttribute('type') == $type &&
$token->isSet('type') &&
($type === null || $token->getAttribute('type') === $type) &&
$token->getAttribute('secret') === self::hash($secret) &&
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string)$token->getId();
}
}
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') === self::hash($secret) &&
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string) $token->getId();
return $token;
}
}

View file

@ -2,26 +2,26 @@
namespace Appwrite\Enum;
enum MessageStatus: string
class MessageStatus
{
/**
* Message that is not ready to be sent
*/
case Draft = 'draft';
public const DRAFT = 'draft';
/**
* Scheduled to be sent for a later time
*/
case Scheduled = 'scheduled';
public const SCHEDULED = 'scheduled';
/**
* Picked up by the worker and starting to send
*/
case Processing = 'processing';
public const PROCESSING = 'processing';
/**
* Sent without errors
*/
case Sent = 'sent';
public const SENT = 'sent';
/**
* Sent with some errors
*/
case Failed = 'failed';
public const FAILED = 'failed';
}

View file

@ -99,26 +99,33 @@ class Schema
/** @var Route $route */
$namespace = $route->getLabel('sdk.namespace', '');
$method = $route->getLabel('sdk.method', '');
$name = $namespace . \ucfirst($method);
$methods = $route->getLabel('sdk.method', '');
if (empty($name)) {
continue;
if (!\is_array($methods)) {
$methods = [$methods];
}
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
foreach ($methods as $method) {
$name = $namespace . \ucfirst($method);
if (empty($name)) {
continue;
}
foreach (Mapper::route($utopia, $route, $complexity) as $field) {
switch ($route->getMethod()) {
case 'GET':
$queries[$name] = $field;
break;
case 'POST':
case 'PUT':
case 'PATCH':
case 'DELETE':
$mutations[$name] = $field;
break;
default:
throw new \Exception("Unsupported method: {$route->getMethod()}");
}
}
}
}

View file

@ -82,6 +82,12 @@ class Specs extends Action
'description' => '',
'in' => 'header',
],
'Session' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Session',
'description' => 'The user session to authenticate with',
'in' => 'header',
]
],
APP_PLATFORM_SERVER => [
'Project' => [
@ -108,6 +114,24 @@ class Specs extends Action
'description' => '',
'in' => 'header',
],
'Session' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Session',
'description' => 'The user session to authenticate with',
'in' => 'header',
],
'ForwardedFor' => [
'type' => 'apiKey',
'name' => 'X-Forwarded-For',
'description' => 'The IP address of the client that made the request',
'in' => 'header',
],
'ForwardedUserAgent' => [
'type' => 'apiKey',
'name' => 'X-Forwarded-User-Agent',
'description' => 'The user agent string of the client that made the request',
'in' => 'header',
],
],
APP_PLATFORM_CONSOLE => [
'Project' => [
@ -173,6 +197,7 @@ class Specs extends Action
if (empty($routeSecurity)) {
$sdkPlaforms[] = APP_PLATFORM_CLIENT;
$sdkPlaforms[] = APP_PLATFORM_SERVER;
}
if (!$route->getLabel('docs', true)) {

View file

@ -124,7 +124,11 @@ class OpenAPI3 extends Format
continue;
}
$id = $route->getLabel('sdk.method', \uniqid());
$method = $route->getLabel('sdk.method', [\uniqid()]);
if (\is_array($method)) {
$method = $method[0];
}
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__ . '/../../../../' . $route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
@ -149,21 +153,26 @@ class OpenAPI3 extends Format
}
if (empty($routeSecurity)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
if (!$route->getLabel('sdk.hideServer', false)) {
$sdkPlatforms[] = APP_PLATFORM_SERVER;
}
if (!$route->getLabel('sdk.hideClient', false)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
}
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($method),
'tags' => [$route->getLabel('sdk.namespace', 'default')],
'description' => ($desc) ? \file_get_contents($desc) : '',
'responses' => [],
'x-appwrite' => [ // Appwrite related metadata
'method' => $route->getLabel('sdk.method', \uniqid()),
'method' => $method,
'weight' => $route->getOrder(),
'cookies' => $route->getLabel('sdk.cookies', false),
'type' => $route->getLabel('sdk.methodType', ''),
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($id) . '.md',
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($method) . '.md',
'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''),
'rate-limit' => $route->getLabel('abuse-limit', 0),
'rate-time' => $route->getLabel('abuse-time', 3600),
@ -423,7 +432,7 @@ class OpenAPI3 extends Format
foreach ($this->enumBlacklist as $blacklist) {
if (
$blacklist['namespace'] == $route->getLabel('sdk.namespace', '')
&& $blacklist['method'] == $route->getLabel('sdk.method', '')
&& $blacklist['method'] == $method
&& $blacklist['parameter'] == $name
) {
$allowed = false;
@ -433,8 +442,8 @@ class OpenAPI3 extends Format
if ($allowed) {
$node['schema']['enum'] = $validator->getList();
$node['schema']['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['schema']['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['schema']['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $method, $name);
$node['schema']['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $method, $name);
}
if ($validator->getType() === 'integer') {
$node['format'] = 'int32';

View file

@ -123,7 +123,11 @@ class Swagger2 extends Format
continue;
}
$id = $route->getLabel('sdk.method', \uniqid());
$method = $route->getLabel('sdk.method', [\uniqid()]);
if (\is_array($method)) {
$method = $method[0];
}
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__ . '/../../../../' . $route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
@ -149,22 +153,23 @@ class Swagger2 extends Format
if (empty($routeSecurity)) {
$sdkPlatforms[] = APP_PLATFORM_CLIENT;
$sdkPlatforms[] = APP_PLATFORM_SERVER;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id),
'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($method),
'consumes' => [],
'produces' => [],
'tags' => [$route->getLabel('sdk.namespace', 'default')],
'description' => ($desc) ? \file_get_contents($desc) : '',
'responses' => [],
'x-appwrite' => [ // Appwrite related metadata
'method' => $route->getLabel('sdk.method', \uniqid()),
'method' => $method,
'weight' => $route->getOrder(),
'cookies' => $route->getLabel('sdk.cookies', false),
'type' => $route->getLabel('sdk.methodType', ''),
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($id) . '.md',
'demo' => Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')) . '/' . Template::fromCamelCaseToDash($method) . '.md',
'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''),
'rate-limit' => $route->getLabel('abuse-limit', 0),
'rate-time' => $route->getLabel('abuse-time', 3600),
@ -424,7 +429,7 @@ class Swagger2 extends Format
// Do not add the enum
$allowed = true;
foreach ($this->enumBlacklist as $blacklist) {
if ($blacklist['namespace'] == $route->getLabel('sdk.namespace', '') && $blacklist['method'] == $route->getLabel('sdk.method', '') && $blacklist['parameter'] == $name) {
if ($blacklist['namespace'] == $route->getLabel('sdk.namespace', '') && $blacklist['method'] == $method && $blacklist['parameter'] == $name) {
$allowed = false;
break;
}
@ -432,8 +437,8 @@ class Swagger2 extends Format
if ($allowed) {
$node['enum'] = $validator->getList();
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $route->getLabel('sdk.method', ''), $name);
$node['x-enum-name'] = $this->getEnumName($route->getLabel('sdk.namespace', ''), $method, $name);
$node['x-enum-keys'] = $this->getEnumKeys($route->getLabel('sdk.namespace', ''), $method, $name);
}
if ($validator->getType() === 'integer') {

View file

@ -25,7 +25,11 @@ class Request extends UtopiaRequest
$parameters = parent::getParams();
if (self::hasFilter() && self::hasRoute()) {
$endpointIdentifier = self::getRoute()->getLabel('sdk.namespace', 'unknown') . '.' . self::getRoute()->getLabel('sdk.method', 'unknown');
$method = self::getRoute()->getLabel('sdk.method', ['unknown']);
if (\is_array($method)) {
$method = $method[0];
}
$endpointIdentifier = self::getRoute()->getLabel('sdk.namespace', 'unknown') . '.' . $method;
$parameters = self::getFilter()->parse($parameters, $endpointIdentifier);
}

View file

@ -160,6 +160,12 @@ class Session extends Model
'default' => false,
'example' => true,
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'Secret used to authenticate the user. Only included if the request was made with an API key',
'default' => '',
'example' => '5e5bb8c16897e',
])
;
}

View file

@ -31,8 +31,8 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Appwrite-Session, X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);
$this->assertEmpty($response['body']);

View file

@ -83,6 +83,9 @@ trait ProjectCustom
'health.read',
'rules.read',
'rules.write',
'sessions.write',
'accounts.write',
'accounts.read',
'targets.read',
'targets.write',
'providers.read',

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,9 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Tests\E2E\Client;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
class AccountConsoleClientTest extends Scope
{

File diff suppressed because it is too large Load diff

View file

@ -6,35 +6,259 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Helpers\ID;
class AccountCustomServerTest extends Scope
{
use AccountBase;
use ProjectCustom;
use SideServer;
public function testCreateAccount(): array
/**
* @depends testCreateAccount
*/
public function testCreateAccountSession($data): array
{
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$email = $data['email'] ?? '';
$password = $data['password'] ?? '';
/**
* Test for SUCCESS
*/
$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'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire']));
$sessionId = $response['body']['$id'];
$session = $response['body']['secret'];
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
$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'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire']));
// already logged in
$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'],
'x-appwrite-session' => $session,
], $this->getHeaders()), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_POST, '/account', [
$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'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => $email,
], $this->getHeaders()), [
'email' => $email . 'x',
'password' => $password,
'name' => $name,
]);
$this->assertEquals(401, $response['headers']['status-code']);
return [];
$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'],
], $this->getHeaders()), [
'email' => $email,
'password' => $password . 'x',
]);
$this->assertEquals(401, $response['headers']['status-code']);
$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'],
], $this->getHeaders()), [
'email' => '',
'password' => '',
]);
$this->assertEquals(400, $response['headers']['status-code']);
return array_merge($data, [
'sessionId' => $sessionId,
'session' => $session,
]);
}
public function testCreateAnonymousAccount()
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
));
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['secret']);
\usleep(1000 * 30); // wait for 30ms to let the shutdown update accessedAt
$userId = $response['body']['userId'];
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId, array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
],
$this->getHeaders(),
));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
}
public function testCreateMagicUrl(): array
{
$email = \time() . 'user@appwrite.io';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/magic-url', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
), [
'userId' => ID::unique(),
'email' => $email,
// 'url' => 'http://localhost/magiclogin',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 64);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$this->assertNotFalse($expireTime);
$secretTest = strpos($lastEmail['text'], 'secret=' . $response['body']['secret'], 0);
$this->assertNotFalse($secretTest);
$userIDTest = strpos($lastEmail['text'], 'userId=' . $response['body']['userId'], 0);
$this->assertNotFalse($userIDTest);
$data['token'] = $token;
$data['id'] = $userId;
$data['email'] = $email;
return $data;
}
/**
* @depends testCreateMagicUrl
*/
public function testCreateSessionWithMagicUrl($data): array
{
$id = $data['id'] ?? '';
$token = $data['token'] ?? '';
$email = $data['email'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
],
$this->getHeaders()
), [
'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']);
$this->assertNotEmpty($response['body']['secret']);
$sessionId = $response['body']['$id'];
$session = $response['body']['secret'];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge(
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session
],
$this->getHeaders()
));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['registration']));
$this->assertEquals($response['body']['email'], $email);
$this->assertTrue($response['body']['emailVerification']);
$data['sessionId'] = $sessionId;
$data['session'] = $session;
return $data;
}
}

View file

@ -243,6 +243,7 @@ trait MessagingBase
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals('my-app', $response['body']['name']);
$this->assertEquals('', $response['body']['description']);
return $response['body'];
}

View file

@ -223,10 +223,10 @@ trait TeamsBaseClient
*/
$secondEmail = uniqid() . 'foe@localhost.test';
$secondName = 'Another Foe';
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
$response = $this->client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
], [
'userId' => 'unique()',
'email' => $secondEmail,
'password' => 'password',

View file

@ -230,6 +230,65 @@ trait UsersBase
}
}
/**
* @depends testCreateUser
*/
public function testCreateToken(array $data): void
{
/**
* Test for SUCCESS
*/
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals($data['userId'], $token['body']['userId']);
$this->assertNotEmpty($token['body']['secret']);
$this->assertNotEmpty($token['body']['expire']);
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'length' => 15,
'expire' => 60,
]);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals($data['userId'], $token['body']['userId']);
$this->assertEquals(15, strlen($token['body']['secret']));
$this->assertNotEmpty($token['body']['expire']);
/**
* Test for FAILURE
*/
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'length' => 1,
'expire' => 1,
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertArrayNotHasKey('userId', $token['body']);
$this->assertArrayNotHasKey('secret', $token['body']);
$token = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/tokens', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'expire' => 999999999999999,
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertArrayNotHasKey('userId', $token['body']);
$this->assertArrayNotHasKey('secret', $token['body']);
}
/**
* Tests all optional parameters of createUser (email, phone, anonymous..)
*

View file

@ -189,8 +189,8 @@ class AuthTest extends TestCase
public function testTokenGenerator(): void
{
$this->assertEquals(\mb_strlen(Auth::tokenGenerator()), 256);
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
$this->assertEquals(\strlen(Auth::tokenGenerator()), 256);
$this->assertEquals(\strlen(Auth::tokenGenerator(5)), 5);
}
public function testCodeGenerator(): void
@ -294,7 +294,8 @@ class AuthTest extends TestCase
]),
];
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), 'token1');
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);