2019-05-09 18:54:39 +12:00
< ? php
2021-06-13 07:37:19 +12:00
use Ahc\Jwt\JWT ;
2020-03-25 06:56:32 +13:00
use Appwrite\Auth\Auth ;
2023-07-15 12:28:45 +12:00
use Appwrite\Auth\OAuth2\Exception as OAuth2Exception ;
2020-03-25 06:56:32 +13:00
use Appwrite\Auth\Validator\Password ;
2022-08-15 03:10:12 +12:00
use Appwrite\Auth\Validator\Phone ;
2021-02-15 06:28:54 +13:00
use Appwrite\Detector\Detector ;
2022-05-19 04:14:21 +12:00
use Appwrite\Event\Event ;
use Appwrite\Event\Mail ;
2022-08-12 11:53:52 +12:00
use Appwrite\Event\Phone as EventPhone ;
use Appwrite\Extend\Exception ;
2021-05-07 10:31:05 +12:00
use Appwrite\Network\Validator\Email ;
2023-08-24 08:23:52 +12:00
use Utopia\Validator\Host ;
use Utopia\Validator\URL ;
2021-08-05 17:06:38 +12:00
use Appwrite\OpenSSL\OpenSSL ;
use Appwrite\Template\Template ;
use Appwrite\URL\URL as URLParser ;
2022-08-12 11:53:52 +12:00
use Appwrite\Utopia\Database\Validator\CustomId ;
2023-05-18 13:11:45 +12:00
use Appwrite\Utopia\Database\Validator\Queries\Identities ;
2023-08-24 08:23:52 +12:00
use Utopia\Database\Validator\Queries ;
use Utopia\Database\Validator\Query\Limit ;
use Utopia\Database\Validator\Query\Offset ;
2022-05-19 04:14:21 +12:00
use Appwrite\Utopia\Request ;
2021-08-05 17:06:38 +12:00
use Appwrite\Utopia\Response ;
2022-05-19 04:14:21 +12:00
use MaxMind\Db\Reader ;
2021-05-07 10:31:05 +12:00
use Utopia\App ;
2022-05-26 01:49:32 +12:00
use Utopia\Audit\Audit as EventAudit ;
2021-08-05 17:06:38 +12:00
use Utopia\Config\Config ;
2022-05-19 04:14:21 +12:00
use Utopia\Database\Database ;
2023-08-24 05:12:43 +12:00
use Utopia\Database\Document ;
2023-08-24 08:23:52 +12:00
use Utopia\Database\DateTime ;
2021-05-07 10:31:05 +12:00
use Utopia\Database\Exception\Duplicate ;
2022-12-15 04:42:25 +13:00
use Utopia\Database\Helpers\ID ;
2022-12-15 05:04:06 +13:00
use Utopia\Database\Helpers\Permission ;
2023-08-24 05:12:43 +12:00
use Utopia\Database\Query ;
2023-08-24 08:23:52 +12:00
use Utopia\Database\Helpers\Role ;
2021-05-07 10:31:05 +12:00
use Utopia\Database\Validator\Authorization ;
use Utopia\Database\Validator\UID ;
2022-05-19 04:14:21 +12:00
use Utopia\Locale\Locale ;
2021-08-05 17:06:38 +12:00
use Utopia\Validator\ArrayList ;
use Utopia\Validator\Assoc ;
use Utopia\Validator\Text ;
use Utopia\Validator\WhiteList ;
2023-08-24 08:23:52 +12:00
use Appwrite\Auth\Validator\PasswordHistory ;
use Appwrite\Auth\Validator\PasswordDictionary ;
use Appwrite\Auth\Validator\PersonalData ;
2019-05-09 18:54:39 +12:00
2022-11-19 09:24:04 +13:00
$oauthDefaultSuccess = '/auth/oauth2/success' ;
$oauthDefaultFailure = '/auth/oauth2/failure' ;
2020-04-09 01:38:36 +12:00
2023-01-11 03:48:45 +13:00
App :: post ( '/v1/account/invite' )
2023-01-12 00:41:33 +13:00
-> desc ( 'Create account using an invite code' )
2021-03-01 07:36:13 +13:00
-> groups ([ 'api' , 'account' , 'auth' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2021-03-01 07:36:13 +13:00
-> label ( 'auth.type' , 'emailPassword' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.create' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-08-17 02:56:05 +12:00
-> label ( 'audits.userId' , '{response.$id}' )
2023-01-12 01:35:07 +13:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_ADMIN ])
2020-01-04 10:00:53 +13:00
-> label ( 'sdk.namespace' , 'account' )
2023-01-11 03:48:45 +13:00
-> label ( 'sdk.method' , 'createWithInviteCode' )
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2022-05-06 20:58:36 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_ACCOUNT )
2020-01-04 10:00:53 +13:00
-> label ( 'abuse-limit' , 10 )
2022-10-04 09:22:28 +13:00
-> param ( 'userId' , '' , new CustomId (), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2021-11-26 09:07:54 +13:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2020-09-11 02:40:14 +12:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' , true )
2023-01-12 00:41:33 +13:00
-> param ( 'code' , '' , new Text ( 128 ), 'An invite code to restrict user signups on the Appwrite console. Users with an invite code will be able to create accounts irrespective of email and IP whitelists.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-01-10 06:09:58 +13:00
-> action ( function ( string $userId , string $email , string $password , string $name , string $code , Request $request , Response $response , Document $project , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
2023-01-12 01:35:07 +13:00
if ( $project -> getId () !== 'console' ) {
throw new Exception ( Exception :: GENERAL_ACCESS_FORBIDDEN );
}
2021-06-04 00:58:40 +12:00
$email = \strtolower ( $email );
2023-01-10 06:09:58 +13:00
2023-08-24 08:23:52 +12:00
$whitelistCodes = ( ! empty ( App :: getEnv ( '_APP_CONSOLE_WHITELIST_CODES' , null ))) ? \explode ( ',' , App :: getEnv ( '_APP_CONSOLE_WHITELIST_CODES' , null )) : [];
2020-06-30 09:43:34 +12:00
2023-01-20 00:56:41 +13:00
if ( empty ( $whitelistCodes )) {
throw new Exception ( Exception :: GENERAL_CODES_DISABLED );
}
2023-08-24 08:23:52 +12:00
if ( ! empty ( $whitelistCodes ) && ! \in_array ( $code , $whitelistCodes )) {
2023-01-20 00:56:41 +13:00
throw new Exception ( Exception :: USER_INVALID_CODE );
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2021-08-06 20:34:17 +12:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-03-01 07:36:13 +13:00
if ( $limit !== 0 ) {
2022-05-16 21:58:17 +12:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-03-01 07:36:13 +13:00
2022-02-27 22:57:09 +13:00
if ( $total >= $limit ) {
2022-07-23 02:38:06 +12:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-03-01 07:36:13 +13:00
}
}
2020-06-30 09:43:34 +12:00
try {
2022-08-15 02:22:38 +12:00
$userId = $userId == 'unique()' ? ID :: unique () : $userId ;
2023-08-24 08:23:52 +12:00
$user = Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , new Document ([
2022-08-15 23:24:31 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 23:24:31 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2020-06-30 09:43:34 +12:00
'email' => $email ,
2023-01-10 06:22:15 +13:00
'emailVerification' => false ,
'status' => true ,
'password' => Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS ),
'hash' => Auth :: DEFAULT_ALGO ,
'hashOptions' => Auth :: DEFAULT_ALGO_OPTIONS ,
'passwordUpdate' => DateTime :: now (),
'registration' => DateTime :: now (),
'reset' => false ,
'name' => $name ,
'prefs' => new \stdClass (),
'sessions' => null ,
'tokens' => null ,
'memberships' => null ,
2023-08-24 08:23:52 +12:00
'search' => implode ( ' ' , [ $userId , $email , $name ])
2023-01-10 06:22:15 +13:00
])));
} catch ( Duplicate $th ) {
throw new Exception ( Exception :: USER_ALREADY_EXISTS );
}
Authorization :: unsetRole ( Role :: guests () -> toString ());
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
Authorization :: setRole ( Role :: users () -> toString ());
$events -> setParam ( 'userId' , $user -> getId ());
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $user , Response :: MODEL_ACCOUNT );
});
2023-01-10 21:53:08 +13:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account' )
2020-01-23 12:32:10 +13:00
-> desc ( 'Create Account' )
2021-03-01 07:36:13 +13:00
-> groups ([ 'api' , 'account' , 'auth' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2021-03-01 07:36:13 +13:00
-> label ( 'auth.type' , 'emailPassword' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.create' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-08-17 02:56:05 +12:00
-> label ( 'audits.userId' , '{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [])
2020-01-04 10:00:53 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'create' )
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-01-04 10:00:53 +13:00
-> label ( 'abuse-limit' , 10 )
2023-01-21 11:22:16 +13:00
-> param ( 'userId' , '' , new CustomId (), 'Unique 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.' )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2023-02-20 20:08:27 +13:00
-> param ( 'password' , '' , fn ( $project , $passwordsDictionary ) => new PasswordDictionary ( $passwordsDictionary , $project -> getAttribute ( 'auths' , [])[ 'passwordDictionary' ] ? ? false ), 'New user password. Must be at least 8 chars.' , false , [ 'project' , 'passwordsDictionary' ])
2020-09-11 02:40:14 +12:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2020-12-27 03:31:53 +13:00
-> inject ( 'project' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $userId , string $email , string $password , string $name , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Event $events ) {
2021-06-04 00:58:40 +12:00
$email = \strtolower ( $email );
2020-06-30 09:43:34 +12:00
if ( 'console' === $project -> getId ()) {
2021-06-04 00:58:40 +12:00
$whitelistEmails = $project -> getAttribute ( 'authWhitelistEmails' );
$whitelistIPs = $project -> getAttribute ( 'authWhitelistIPs' );
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $whitelistEmails ) && ! \in_array ( $email , $whitelistEmails ) && ! \in_array ( strtoupper ( $email ), $whitelistEmails )) {
2022-07-23 02:38:06 +12:00
throw new Exception ( Exception :: USER_EMAIL_NOT_WHITELISTED );
2020-01-04 10:00:53 +13:00
}
2023-08-24 08:23:52 +12:00
if ( ! empty ( $whitelistIPs ) && ! \in_array ( $request -> getIP (), $whitelistIPs )) {
2022-07-23 02:38:06 +12:00
throw new Exception ( Exception :: USER_IP_NOT_WHITELISTED );
2020-01-04 10:00:53 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2021-08-06 20:34:17 +12:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-03-01 07:36:13 +13:00
if ( $limit !== 0 ) {
2022-05-16 21:58:17 +12:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-03-01 07:36:13 +13:00
2022-02-27 22:57:09 +13:00
if ( $total >= $limit ) {
2022-07-23 02:38:06 +12:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-03-01 07:36:13 +13:00
}
}
2023-05-18 13:11:45 +12:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2023-08-24 08:23:52 +12:00
if ( $identityWithMatchingEmail !== false && ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
2023-07-20 10:24:32 +12:00
if ( $project -> getAttribute ( 'auths' , [])[ 'personalDataCheck' ] ? ? false ) {
2023-04-14 08:20:03 +12:00
$personalDataValidator = new PersonalData ( $userId , $email , $name , null );
2023-08-24 08:23:52 +12:00
if ( ! $personalDataValidator -> isValid ( $password )) {
2023-04-14 08:20:03 +12:00
throw new Exception ( Exception :: USER_PASSWORD_PERSONAL_DATA );
}
}
2022-12-18 19:31:14 +13:00
$passwordHistory = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
2022-12-18 19:27:41 +13:00
$password = Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS );
2020-06-30 09:43:34 +12:00
try {
2022-08-15 02:22:38 +12:00
$userId = $userId == 'unique()' ? ID :: unique () : $userId ;
2023-07-07 12:12:39 +12:00
$user -> setAttributes ([
2022-08-15 23:24:31 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 23:24:31 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2020-06-30 09:43:34 +12:00
'email' => $email ,
'emailVerification' => false ,
2021-07-14 23:02:12 +12:00
'status' => true ,
2022-12-18 19:27:41 +13:00
'password' => $password ,
2023-02-20 14:51:56 +13:00
'passwordHistory' => $passwordHistory > 0 ? [ $password ] : [],
'passwordUpdate' => DateTime :: now (),
2022-06-14 00:53:28 +12:00
'hash' => Auth :: DEFAULT_ALGO ,
'hashOptions' => Auth :: DEFAULT_ALGO_OPTIONS ,
2022-07-14 02:02:49 +12:00
'registration' => DateTime :: now (),
2020-06-30 09:43:34 +12:00
'reset' => false ,
'name' => $name ,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass (),
2022-04-26 22:36:49 +12:00
'sessions' => null ,
2022-04-27 23:06:53 +12:00
'tokens' => null ,
2022-04-28 00:44:47 +12:00
'memberships' => null ,
2023-07-07 12:12:39 +12:00
'search' => implode ( ' ' , [ $userId , $email , $name ]),
'accessedAt' => DateTime :: now (), // Add this here to make sure it's returned in the response
]);
2023-08-24 08:23:52 +12:00
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2020-06-30 09:43:34 +12:00
} catch ( Duplicate $th ) {
2022-07-23 02:38:06 +12:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS );
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2022-08-19 16:04:33 +12:00
Authorization :: unsetRole ( Role :: guests () -> toString ());
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
Authorization :: setRole ( Role :: users () -> toString ());
2020-11-21 10:02:26 +13:00
2022-04-19 21:30:42 +12:00
$events -> setParam ( 'userId' , $user -> getId ());
2022-04-04 18:30:07 +12:00
2022-09-07 23:02:36 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2022-09-07 23:07:40 +12:00
-> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2020-01-05 04:45:28 +13:00
2022-06-14 20:17:50 +12:00
App :: post ( '/v1/account/sessions/email' )
2022-07-08 10:23:52 +12:00
-> alias ( '/v1/account/sessions' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Create Email Session' )
2022-12-11 21:33:52 +13:00
-> groups ([ 'api' , 'account' , 'auth' , 'session' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2021-03-01 10:22:03 +13:00
-> label ( 'auth.type' , 'emailPassword' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.create' )
2022-08-13 01:21:32 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [])
2020-01-05 04:45:28 +13:00
-> label ( 'sdk.namespace' , 'account' )
2022-06-14 20:17:50 +12:00
-> label ( 'sdk.method' , 'createEmailSession' )
-> label ( 'sdk.description' , '/docs/references/account/create-session-email.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION )
2020-01-05 04:45:28 +13:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2021-11-26 09:07:54 +13:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-11-01 03:54:15 +13:00
-> inject ( 'project' )
2020-12-27 03:31:53 +13:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $email , string $password , Request $request , Response $response , Document $user , Database $dbForProject , Document $project , Locale $locale , Reader $geodb , Event $events ) {
2023-08-24 08:23:52 +12:00
2021-06-04 01:03:51 +12:00
$email = \strtolower ( $email );
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2021-08-05 17:06:38 +12:00
2022-05-13 04:25:36 +12:00
$profile = $dbForProject -> findOne ( 'users' , [
2022-08-12 11:53:52 +12:00
Query :: equal ( 'email' , [ $email ]),
]);
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! $profile || empty ( $profile -> getAttribute ( 'passwordUpdate' )) || ! Auth :: passwordVerify ( $password , $profile -> getAttribute ( 'password' ), $profile -> getAttribute ( 'hash' ), $profile -> getAttribute ( 'hashOptions' ))) {
2022-08-15 04:23:30 +12:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2021-07-14 23:02:12 +12:00
if ( false === $profile -> getAttribute ( 'status' )) { // Account is blocked
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: USER_BLOCKED ); // User is in status blocked
2020-12-28 00:57:42 +13:00
}
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2022-11-14 22:42:18 +13:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-11-01 03:54:15 +13:00
2021-02-15 06:28:54 +13:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2023-01-11 19:33:55 +13:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2020-06-30 09:43:34 +12:00
$secret = Auth :: tokenGenerator ();
2021-02-15 06:28:54 +13:00
$session = new Document ( array_merge (
[
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2023-07-07 12:12:39 +12:00
'userId' => $user -> getId (),
'userInternalId' => $user -> getInternalId (),
2021-02-20 01:12:47 +13:00
'provider' => Auth :: SESSION_PROVIDER_EMAIL ,
2021-02-19 23:02:02 +13:00
'providerUid' => $email ,
2021-02-15 06:28:54 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2022-05-24 02:54:50 +12:00
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
2021-02-15 06:28:54 +13:00
));
2020-10-31 08:53:27 +13:00
2023-07-07 12:12:39 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-01-04 10:00:53 +13:00
2022-05-06 20:58:36 +12:00
// Re-hash if not using recommended algo
2023-07-07 12:12:39 +12:00
if ( $user -> getAttribute ( 'hash' ) !== Auth :: DEFAULT_ALGO ) {
$user
2022-05-06 20:58:36 +12:00
-> setAttribute ( 'password' , Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS ))
-> setAttribute ( 'hash' , Auth :: DEFAULT_ALGO )
-> setAttribute ( 'hashOptions' , Auth :: DEFAULT_ALGO_OPTIONS );
2023-07-07 12:12:39 +12:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-05-06 20:58:36 +12:00
}
2023-07-07 12:12:39 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2020-01-05 04:45:28 +13:00
2022-08-02 21:21:53 +12:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
2023-07-07 12:12:39 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2020-01-12 02:58:02 +13:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2020-01-04 10:00:53 +13:00
$response
2023-08-24 08:23:52 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]))
;
2020-01-04 10:00:53 +13:00
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2023-07-07 12:12:39 +12:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2023-08-24 08:23:52 +12:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
;
2020-10-31 08:53:27 +13:00
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-06-17 21:33:57 +12:00
2020-10-31 08:53:27 +13:00
$session
-> setAttribute ( 'current' , true )
2021-06-17 21:33:57 +12:00
-> setAttribute ( 'countryName' , $countryName )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'expire' , $expire )
;
2021-08-05 17:06:38 +12:00
2022-04-04 18:30:07 +12:00
$events
2023-07-07 12:12:39 +12:00
-> setParam ( 'userId' , $user -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'sessionId' , $session -> getId ())
;
2022-04-04 18:30:07 +12:00
2021-07-26 02:47:18 +12:00
$response -> dynamic ( $session , Response :: MODEL_SESSION );
2020-12-27 03:31:53 +13:00
});
2020-01-04 10:00:53 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/sessions/oauth2/:provider' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Create OAuth2 Session' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2023-08-24 08:23:52 +12:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [])
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-02-17 00:41:03 +13:00
-> label ( 'sdk.method' , 'createOAuth2Session' )
-> label ( 'sdk.description' , '/docs/references/account/create-session-oauth2.md' )
2021-03-05 18:30:34 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_MOVED_PERMANENTLY )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_HTML )
2020-04-11 06:59:14 +12:00
-> label ( 'sdk.methodType' , 'webAuth' )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
2023-08-24 08:23:52 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'authProviders' )), true ), 'OAuth2 Provider. Currently, supported providers are: ' . \implode ( ', ' , \array_keys ( \array_filter ( Config :: getParam ( 'authProviders' ), fn ( $node ) => ( ! $node [ 'mock' ])))) . '.' )
-> param ( 'success' , '' , fn ( $clients ) => new Host ( $clients ), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , true , [ 'clients' ])
-> param ( 'failure' , '' , fn ( $clients ) => new Host ( $clients ), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , true , [ 'clients' ])
-> param ( 'scopes' , [], new ArrayList ( new Text ( APP_LIMIT_ARRAY_ELEMENT_SIZE ), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2022-05-31 23:11:55 +12:00
-> action ( function ( string $provider , string $success , string $failure , array $scopes , Request $request , Response $response , Document $project ) use ( $oauthDefaultSuccess , $oauthDefaultFailure ) {
2023-08-24 08:23:52 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2022-11-19 02:13:33 +13:00
2023-08-24 08:23:52 +12:00
$callback = $protocol . '://' . $request -> getHostname () . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
$providerEnabled = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Enabled' ] ? ? false ;
2023-01-09 23:44:28 +13:00
2023-08-24 08:23:52 +12:00
if ( ! $providerEnabled ) {
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.' );
2023-01-09 23:35:03 +13:00
}
2023-01-09 23:44:28 +13:00
2023-08-24 08:23:52 +12:00
$appId = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = App :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
2020-06-30 09:43:34 +12:00
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , \hex2bin ( $appSecret [ 'iv' ]), \hex2bin ( $appSecret [ 'tag' ]));
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( empty ( $appId ) || empty ( $appSecret )) {
2023-08-24 08:23:52 +12:00
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please configure the provider app ID and app secret key from your ' . APP_NAME . ' console to continue.' );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst ( $provider );
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! \class_exists ( $className )) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
2022-05-24 02:54:50 +12:00
if ( empty ( $success )) {
2023-08-24 08:23:52 +12:00
$success = $protocol . '://' . $request -> getHostname () . $oauthDefaultSuccess ;
2021-08-31 19:04:17 +12:00
}
2022-05-24 02:54:50 +12:00
if ( empty ( $failure )) {
2023-08-24 08:23:52 +12:00
$failure = $protocol . '://' . $request -> getHostname () . $oauthDefaultFailure ;
2021-08-31 19:04:17 +12:00
}
2021-08-06 22:48:50 +12:00
$oauth2 = new $className ( $appId , $appSecret , $callback , [ 'success' => $success , 'failure' => $failure ], $scopes );
2020-06-30 09:43:34 +12:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $oauth2 -> getLoginURL ());
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/sessions/oauth2/callback/:provider/:projectId' )
2020-02-17 00:41:03 +13:00
-> desc ( 'OAuth2 Callback' )
2023-07-25 13:38:54 +12:00
-> groups ([ 'account' ])
2023-08-24 08:23:52 +12:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2021-12-11 01:27:11 +13:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project ID.' )
2023-08-07 22:24:00 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'authProviders' )), true ), 'OAuth2 provider.' )
2023-07-28 12:12:15 +12:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code.' , true )
2020-09-11 02:40:14 +12:00
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2023-07-28 12:12:15 +12:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-28 12:12:15 +12:00
-> action ( function ( string $projectId , string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response ) {
2023-08-24 08:23:52 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request -> getHostname ();
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2023-08-24 08:23:52 +12:00
-> redirect ( $protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query ([
2023-07-28 12:12:15 +12:00
'project' => $projectId ,
'code' => $code ,
'state' => $state ,
'error' => $error ,
2023-08-24 08:23:52 +12:00
'error_description' => $error_description
2023-07-28 12:12:15 +12:00
]));
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account/sessions/oauth2/callback/:provider/:projectId' )
2020-05-30 00:02:53 +12:00
-> desc ( 'OAuth2 Callback' )
2023-07-25 13:38:54 +12:00
-> groups ([ 'account' ])
2023-08-24 08:23:52 +12:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-05-30 00:02:53 +12:00
-> label ( 'scope' , 'public' )
-> label ( 'origin' , '*' )
-> label ( 'docs' , false )
2021-12-11 01:27:11 +13:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project ID.' )
2023-08-07 22:24:00 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'authProviders' )), true ), 'OAuth2 provider.' )
2023-07-28 12:12:15 +12:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code.' , true )
2020-09-11 02:40:14 +12:00
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2023-07-28 12:12:15 +12:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-28 12:12:15 +12:00
-> action ( function ( string $projectId , string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response ) {
2023-08-24 08:23:52 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request -> getHostname ();
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2023-08-24 08:23:52 +12:00
-> redirect ( $protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query ([
2023-07-28 12:12:15 +12:00
'project' => $projectId ,
'code' => $code ,
'state' => $state ,
'error' => $error ,
2023-08-24 08:23:52 +12:00
'error_description' => $error_description
2023-07-28 12:12:15 +12:00
]));
2020-12-27 03:31:53 +13:00
});
2020-05-30 00:02:53 +12:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/sessions/oauth2/:provider/redirect' )
2020-02-17 00:41:03 +13:00
-> desc ( 'OAuth2 Redirect' )
2022-12-11 21:33:52 +13:00
-> groups ([ 'api' , 'account' , 'session' ])
2023-08-24 08:23:52 +12:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.create' )
2022-08-16 05:04:23 +12:00
-> label ( 'audits.resource' , 'user/{user.$id}' )
2023-02-05 20:02:56 +13:00
-> label ( 'audits.userId' , '{user.$id}' )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
-> label ( 'docs' , false )
2023-08-07 22:24:00 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'authProviders' )), true ), 'OAuth2 provider.' )
2023-07-28 12:12:15 +12:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code.' , true )
2020-09-11 02:40:14 +12:00
-> param ( 'state' , '' , new Text ( 2048 ), 'OAuth2 state params.' , true )
2023-07-28 12:12:15 +12:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2020-12-27 03:31:53 +13:00
-> inject ( 'geodb' )
2021-05-28 18:56:11 +12:00
-> inject ( 'events' )
2023-07-28 12:12:15 +12:00
-> action ( function ( string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response , Document $project , Document $user , Database $dbForProject , Reader $geodb , Event $events ) use ( $oauthDefaultSuccess ) {
2023-08-24 08:23:52 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2023-08-24 08:23:52 +12:00
$callback = $protocol . '://' . $request -> getHostname () . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
2020-06-30 09:43:34 +12:00
$defaultState = [ 'success' => $project -> getAttribute ( 'url' , '' ), 'failure' => '' ];
$validateURL = new URL ();
2023-08-24 08:23:52 +12:00
$appId = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
$providerEnabled = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Enabled' ] ? ? false ;
2023-01-10 17:52:21 +13:00
2023-08-24 08:23:52 +12:00
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst ( $provider );
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! \class_exists ( $className )) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-08-07 22:24:00 +12:00
$providers = Config :: getParam ( 'authProviders' );
2023-07-15 12:28:45 +12:00
$providerName = $providers [ $provider ][ 'name' ] ? ? '' ;
/** @var Appwrite\Auth\OAuth2 $oauth2 */
2021-08-06 22:48:50 +12:00
$oauth2 = new $className ( $appId , $appSecret , $callback );
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $state )) {
2020-06-30 09:43:34 +12:00
try {
$state = \array_merge ( $defaultState , $oauth2 -> parseState ( $state ));
2023-07-15 12:28:45 +12:00
} catch ( \Exception $exception ) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed to parse login state params as passed from OAuth2 provider' );
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
} else {
$state = $defaultState ;
}
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! $validateURL -> isValid ( $state [ 'success' ])) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: PROJECT_INVALID_SUCCESS_URL );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $state [ 'failure' ]) && ! $validateURL -> isValid ( $state [ 'failure' ])) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: PROJECT_INVALID_FAILURE_URL );
2020-06-30 09:43:34 +12:00
}
2023-07-15 12:28:45 +12:00
$failure = [];
2023-08-24 08:23:52 +12:00
if ( ! empty ( $state [ 'failure' ])) {
2023-07-15 12:28:45 +12:00
$failure = URLParser :: parse ( $state [ 'failure' ]);
}
$failureRedirect = ( function ( string $type , ? string $message = null , ? int $code = null ) use ( $failure , $response ) {
$exception = new Exception ( $type , $message , $code );
2023-08-24 08:23:52 +12:00
if ( ! empty ( $failure )) {
2023-07-15 12:28:45 +12:00
$query = URLParser :: parseQuery ( $failure [ 'query' ]);
$query [ 'error' ] = json_encode ([
'message' => $exception -> getMessage (),
'type' => $exception -> getType (),
2023-08-24 08:23:52 +12:00
'code' => ! \is_null ( $code ) ? $code : $exception -> getCode (),
2023-07-15 12:28:45 +12:00
]);
$failure [ 'query' ] = URLParser :: unparseQuery ( $query );
$response -> redirect ( URLParser :: unparse ( $failure ), 301 );
}
2021-08-05 17:06:38 +12:00
2023-07-15 12:28:45 +12:00
throw $exception ;
});
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! $providerEnabled ) {
$failureRedirect ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.' );
2023-07-15 12:28:45 +12:00
}
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $error )) {
$message = 'The ' . $providerName . ' OAuth2 provider returned an error: ' . $error ;
if ( ! empty ( $error_description )) {
$message .= ': ' . $error_description ;
2020-01-06 00:29:42 +13:00
}
2023-07-28 12:12:15 +12:00
$failureRedirect ( Exception :: USER_OAUTH2_PROVIDER_ERROR , $message );
}
2020-01-06 00:29:42 +13:00
2023-07-28 12:12:15 +12:00
if ( empty ( $code )) {
$failureRedirect ( Exception :: USER_OAUTH2_PROVIDER_ERROR , 'Missing OAuth2 code. Please contact the Appwrite team for additional support.' );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-08-24 08:23:52 +12:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = App :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
2023-07-15 12:28:45 +12:00
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , \hex2bin ( $appSecret [ 'iv' ]), \hex2bin ( $appSecret [ 'tag' ]));
2020-06-30 09:43:34 +12:00
}
2021-08-05 17:06:38 +12:00
2023-07-15 12:28:45 +12:00
$accessToken = '' ;
$refreshToken = '' ;
$accessTokenExpiry = 0 ;
2020-01-06 00:29:42 +13:00
2023-07-15 12:28:45 +12:00
try {
$accessToken = $oauth2 -> getAccessToken ( $code );
$refreshToken = $oauth2 -> getRefreshToken ( $code );
$accessTokenExpiry = $oauth2 -> getAccessTokenExpiry ( $code );
} catch ( OAuth2Exception $ex ) {
$failureRedirect (
$ex -> getType (),
2023-08-24 08:23:52 +12:00
'Failed to obtain access token. The ' . $providerName . ' OAuth2 provider returned an error: ' . $ex -> getMessage (),
2023-07-15 12:28:45 +12:00
$ex -> getCode (),
);
}
2020-01-06 00:29:42 +13:00
2023-07-15 12:28:45 +12:00
$oauth2ID = $oauth2 -> getUserID ( $accessToken );
if ( empty ( $oauth2ID )) {
$failureRedirect ( Exception :: USER_MISSING_ID );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-05-18 13:11:45 +12:00
$name = $oauth2 -> getUserName ( $accessToken );
$email = $oauth2 -> getUserEmail ( $accessToken );
// Check if this identity is connected to a different user
2023-08-24 08:23:52 +12:00
if ( ! $user -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
$userId = $user -> getId ();
$identitiesWithMatchingEmail = $dbForProject -> find ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
Query :: notEqual ( 'userId' , $userId ),
]);
2023-08-24 08:23:52 +12:00
if ( ! empty ( $identitiesWithMatchingEmail )) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS );
}
}
2021-05-07 10:31:05 +12:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-11-14 22:30:45 +13:00
$current = Auth :: sessionVerify ( $sessions , Auth :: $secret , $authDuration );
2020-01-06 00:29:42 +13:00
2021-08-05 17:06:38 +12:00
if ( $current ) { // Delete current session of new one.
2022-04-04 21:59:32 +12:00
$currentDocument = $dbForProject -> getDocument ( 'sessions' , $current );
2023-08-24 08:23:52 +12:00
if ( ! $currentDocument -> isEmpty ()) {
2022-04-04 21:59:32 +12:00
$dbForProject -> deleteDocument ( 'sessions' , $currentDocument -> getId ());
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-05-07 10:31:05 +12:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-07-07 12:12:39 +12:00
if ( $user -> isEmpty ()) {
$session = $dbForProject -> findOne ( 'sessions' , [ // Get user by provider id
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
]);
2023-08-24 08:23:52 +12:00
if ( $session !== false && ! $session -> isEmpty ()) {
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $dbForProject -> getDocument ( 'users' , $session -> getAttribute ( 'userId' )) -> getArrayCopy ());
}
}
2020-01-06 00:29:42 +13:00
2021-07-18 09:21:33 +12:00
if ( $user === false || $user -> isEmpty ()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
2023-05-02 03:51:31 +12:00
if ( empty ( $email )) {
2023-05-02 01:54:33 +12:00
throw new Exception ( Exception :: USER_UNAUTHORIZED , 'OAuth provider failed to return email.' );
}
2022-05-16 21:34:00 +12:00
/**
* Is verified is not used yet , since we don ' t know after an accout is created anymore if it was verified or not .
*/
2022-05-08 03:43:55 +12:00
$isVerified = $oauth2 -> isEmailVerified ( $accessToken );
2020-01-06 00:29:42 +13:00
2023-07-07 12:12:39 +12:00
$userWithEmail = $dbForProject -> findOne ( 'users' , [
2022-08-12 11:53:52 +12:00
Query :: equal ( 'email' , [ $email ]),
]);
2023-08-24 08:23:52 +12:00
if ( $userWithEmail !== false && ! $userWithEmail -> isEmpty ()) {
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $userWithEmail -> getArrayCopy ());
}
2020-01-06 00:29:42 +13:00
2023-05-18 13:11:45 +12:00
// If user is not found, check if there is an identity with the same provider user ID
if ( $user === false || $user -> isEmpty ()) {
$identity = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
]);
2023-08-24 08:23:52 +12:00
if ( $identity !== false && ! $identity -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
$user = $dbForProject -> getDocument ( 'users' , $identity -> getAttribute ( 'userId' ));
}
}
if ( $user === false || $user -> isEmpty ()) { // Last option -> create the user
2021-08-06 20:34:17 +12:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-05 17:06:38 +12:00
2021-03-01 07:36:13 +13:00
if ( $limit !== 0 ) {
2022-05-16 21:58:17 +12:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-07-18 09:21:33 +12:00
2022-02-27 22:57:09 +13:00
if ( $total >= $limit ) {
2023-07-15 12:28:45 +12:00
$failureRedirect ( Exception :: USER_COUNT_EXCEEDED );
2021-03-01 07:36:13 +13:00
}
}
2021-08-05 17:06:38 +12:00
2023-05-18 13:11:45 +12:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2023-08-24 08:23:52 +12:00
if ( $identityWithMatchingEmail !== false && ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
2022-12-18 17:38:27 +13:00
2020-06-30 09:43:34 +12:00
try {
2022-08-15 02:22:38 +12:00
$userId = ID :: unique ();
2023-07-07 12:12:39 +12:00
$user -> setAttributes ([
2022-08-15 02:22:38 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 02:22:38 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2020-06-30 09:43:34 +12:00
'email' => $email ,
2022-05-16 21:34:00 +12:00
'emailVerification' => true ,
2021-07-14 23:02:12 +12:00
'status' => true , // Email should already be authenticated by OAuth2 provider
2023-07-15 11:17:05 +12:00
'password' => null ,
2022-05-05 02:37:37 +12:00
'hash' => Auth :: DEFAULT_ALGO ,
'hashOptions' => Auth :: DEFAULT_ALGO_OPTIONS ,
2022-07-04 21:55:11 +12:00
'passwordUpdate' => null ,
2022-07-14 02:02:49 +12:00
'registration' => DateTime :: now (),
2020-06-30 09:43:34 +12:00
'reset' => false ,
'name' => $name ,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass (),
2022-04-26 22:36:49 +12:00
'sessions' => null ,
2022-04-27 23:06:53 +12:00
'tokens' => null ,
2022-04-28 00:44:47 +12:00
'memberships' => null ,
2023-08-24 08:23:52 +12:00
'search' => implode ( ' ' , [ $userId , $email , $name ])
2023-07-07 12:12:39 +12:00
]);
2023-08-24 08:23:52 +12:00
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2020-06-30 09:43:34 +12:00
} catch ( Duplicate $th ) {
2023-07-15 12:28:45 +12:00
$failureRedirect ( Exception :: USER_ALREADY_EXISTS );
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2023-05-18 13:11:45 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
Authorization :: setRole ( Role :: users () -> toString ());
2021-07-14 23:02:12 +12:00
if ( false === $user -> getAttribute ( 'status' )) { // Account is blocked
2023-07-15 12:28:45 +12:00
$failureRedirect ( Exception :: USER_BLOCKED ); // User is in status blocked
2020-12-28 00:57:42 +13:00
}
2023-05-18 13:11:45 +12:00
$identity = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'userInternalId' , [ $user -> getInternalId ()]),
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
]);
if ( $identity === false || $identity -> isEmpty ()) {
// Before creating the identity, check if the email is already associated with another user
$userId = $user -> getId ();
$identitiesWithMatchingEmail = $dbForProject -> find ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
Query :: notEqual ( 'userId' , $user -> getId ()),
]);
2023-08-24 08:23:52 +12:00
if ( ! empty ( $identitiesWithMatchingEmail )) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
$dbForProject -> createDocument ( 'identities' , new Document ([
'$id' => ID :: unique (),
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
],
'userInternalId' => $user -> getInternalId (),
'userId' => $userId ,
'provider' => $provider ,
'providerUid' => $oauth2ID ,
'providerEmail' => $email ,
'providerAccessToken' => $accessToken ,
'providerRefreshToken' => $refreshToken ,
2023-08-24 08:23:52 +12:00
'providerAccessTokenExpiry' => DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ),
2023-05-18 13:11:45 +12:00
]));
} else {
$identity
-> setAttribute ( 'providerAccessToken' , $accessToken )
-> setAttribute ( 'providerRefreshToken' , $refreshToken )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'providerAccessTokenExpiry' , DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ));
2023-05-18 13:11:45 +12:00
$dbForProject -> updateDocument ( 'identities' , $identity -> getId (), $identity );
}
2020-06-30 09:43:34 +12:00
// Create session token, verify user account and update OAuth2 ID and Access Token
2022-11-14 22:42:18 +13:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2021-02-15 06:28:54 +13:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2020-06-30 09:43:34 +12:00
$secret = Auth :: tokenGenerator ();
2023-05-23 00:48:52 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2022-07-05 22:59:03 +12:00
2021-02-15 06:28:54 +13:00
$session = new Document ( array_merge ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2022-08-15 23:24:31 +12:00
'userId' => $user -> getId (),
'userInternalId' => $user -> getInternalId (),
2021-02-19 23:02:02 +13:00
'provider' => $provider ,
'providerUid' => $oauth2ID ,
2022-02-01 09:10:12 +13:00
'providerAccessToken' => $accessToken ,
'providerRefreshToken' => $refreshToken ,
2023-08-24 08:23:52 +12:00
'providerAccessTokenExpiry' => DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ),
2020-11-13 00:54:16 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
2021-02-15 06:28:54 +13:00
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2021-02-15 21:16:23 +13:00
], $detector -> getOS (), $detector -> getClient (), $detector -> getDevice ()));
2020-10-31 08:53:27 +13:00
2023-06-23 12:46:09 +12:00
if ( empty ( $user -> getAttribute ( 'email' ))) {
$user -> setAttribute ( 'email' , $oauth2 -> getUserEmail ( $accessToken ));
}
2021-02-17 04:51:08 +13:00
2023-06-23 12:46:09 +12:00
if ( empty ( $user -> getAttribute ( 'name' ))) {
$user -> setAttribute ( 'name' , $oauth2 -> getUserName ( $accessToken ));
2021-02-17 04:51:08 +13:00
}
2020-06-30 09:43:34 +12:00
$user
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'status' , true )
;
2020-06-30 09:43:34 +12:00
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-06-30 09:43:34 +12:00
2022-04-26 22:46:35 +12:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-08-02 21:21:53 +12:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2021-07-18 09:21:33 +12:00
2022-04-04 21:59:32 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2020-06-30 09:43:34 +12:00
2022-11-04 04:03:39 +13:00
$session -> setAttribute ( 'expire' , $expire );
2022-04-04 18:30:07 +12:00
$events
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
2023-08-24 08:23:52 +12:00
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ))
;
2022-04-04 18:30:07 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-04-19 21:30:42 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]));
2020-01-06 00:29:42 +13:00
}
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
// Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing
2021-08-31 19:04:17 +12:00
if ( parse_url ( $state [ 'success' ], PHP_URL_PATH ) === $oauthDefaultSuccess ) {
2020-06-30 09:43:34 +12:00
$state [ 'success' ] = URLParser :: parse ( $state [ 'success' ]);
$query = URLParser :: parseQuery ( $state [ 'success' ][ 'query' ]);
$query [ 'project' ] = $project -> getId ();
2020-07-01 18:35:57 +12:00
$query [ 'domain' ] = Config :: getParam ( 'cookieDomain' );
2020-06-30 09:43:34 +12:00
$query [ 'key' ] = Auth :: $cookieName ;
$query [ 'secret' ] = Auth :: encodeSession ( $user -> getId (), $secret );
$state [ 'success' ][ 'query' ] = URLParser :: unparseQuery ( $query );
$state [ 'success' ] = URLParser :: unparse ( $state [ 'success' ]);
}
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2022-07-13 01:32:39 +12:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2023-08-24 08:23:52 +12:00
-> redirect ( $state [ 'success' ])
;
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2023-05-18 13:11:45 +12:00
App :: get ( '/v1/account/identities' )
-> desc ( 'List Identities' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'usage.metric' , 'users.{scope}.requests.read' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'listIdentities' )
-> label ( 'sdk.description' , '/docs/references/account/list-identities.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_IDENTITY_LIST )
-> label ( 'sdk.offline.model' , '/account/identities' )
2023-08-24 08:23:52 +12:00
-> param ( 'queries' , [], new Identities (), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode ( ', ' , Identities :: ALLOWED_ATTRIBUTES ), true )
2023-05-18 13:11:45 +12:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> action ( function ( array $queries , Response $response , Document $user , Database $dbForProject ) {
2023-08-24 08:23:52 +12:00
2023-05-18 13:11:45 +12:00
$queries = Query :: parseQueries ( $queries );
$queries [] = Query :: equal ( 'userInternalId' , [ $user -> getInternalId ()]);
// Get cursor document if there was a cursor query
$cursor = Query :: getByType ( $queries , [ Query :: TYPE_CURSORAFTER , Query :: TYPE_CURSORBEFORE ]);
$cursor = reset ( $cursor );
if ( $cursor ) {
/** @var Query $cursor */
$identityId = $cursor -> getValue ();
$cursorDocument = $dbForProject -> getDocument ( 'identities' , $identityId );
if ( $cursorDocument -> isEmpty ()) {
throw new Exception ( Exception :: GENERAL_CURSOR_NOT_FOUND , " Identity ' { $identityId } ' for the 'cursor' value not found. " );
}
$cursor -> setValue ( $cursorDocument );
}
$filterQueries = Query :: groupByType ( $queries )[ 'filters' ];
$results = $dbForProject -> find ( 'identities' , $queries );
$total = $dbForProject -> count ( 'identities' , $filterQueries , APP_LIMIT_COUNT );
$response -> dynamic ( new Document ([
'identities' => $results ,
'total' => $total ,
]), Response :: MODEL_IDENTITY_LIST );
});
App :: delete ( '/v1/account/identities/:identityId' )
-> desc ( 'Delete Identity' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'event' , 'users.[userId].identities.[identityId].delete' )
-> label ( 'audits.event' , 'identity.delete' )
-> label ( 'audits.resource' , 'identity/{request.$identityId}' )
-> label ( 'audits.userId' , '{user.$id}' )
-> label ( 'usage.metric' , 'identities.{scope}.requests.delete' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'deleteIdentity' )
-> label ( 'sdk.description' , '/docs/references/account/delete-identity.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_NOCONTENT )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2023-08-18 13:48:21 +12:00
-> param ( 'identityId' , '' , new UID (), 'Identity ID.' )
2023-05-18 13:11:45 +12:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> action ( function ( string $identityId , Response $response , Database $dbForProject ) {
2023-08-24 08:23:52 +12:00
2023-05-18 13:11:45 +12:00
$identity = $dbForProject -> getDocument ( 'identities' , $identityId );
if ( $identity -> isEmpty ()) {
throw new Exception ( Exception :: USER_IDENTITY_NOT_FOUND );
}
$dbForProject -> deleteDocument ( 'identities' , $identityId );
return $response -> noContent ();
});
2021-08-31 19:13:30 +12:00
App :: post ( '/v1/account/sessions/magic-url' )
-> desc ( 'Create Magic URL session' )
2021-08-30 22:44:52 +12:00
-> groups ([ 'api' , 'account' ])
2021-08-30 23:09:45 +12:00
-> label ( 'scope' , 'public' )
2021-08-31 21:22:48 +12:00
-> label ( 'auth.type' , 'magic-url' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2021-08-30 22:44:52 +12:00
-> label ( 'sdk.auth' , [])
-> label ( 'sdk.namespace' , 'account' )
2021-08-31 19:13:30 +12:00
-> label ( 'sdk.method' , 'createMagicURLSession' )
-> label ( 'sdk.description' , '/docs/references/account/create-magic-url-session.md' )
2021-08-30 22:44:52 +12:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TOKEN )
-> label ( 'abuse-limit' , 10 )
2021-08-31 17:57:07 +12:00
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2023-01-21 11:22:16 +13:00
-> param ( 'userId' , '' , new CustomId (), 'Unique 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.' )
2021-08-30 22:44:52 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2023-08-24 08:23:52 +12:00
-> param ( 'url' , '' , fn ( $clients ) => new Host ( $clients ), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , true , [ 'clients' ])
2021-08-30 22:44:52 +12:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2021-08-30 22:44:52 +12:00
-> inject ( 'project' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2021-08-30 22:44:52 +12:00
-> inject ( 'locale' )
-> inject ( 'events' )
-> inject ( 'mails' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $userId , string $email , string $url , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Locale $locale , Event $events , Mail $mails ) {
2023-08-24 08:23:52 +12:00
2022-05-24 02:54:50 +12:00
if ( empty ( App :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP disabled' );
2021-08-31 19:32:58 +12:00
}
2021-12-11 06:52:33 +13:00
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2021-08-30 22:44:52 +12:00
2023-07-07 12:12:39 +12:00
$result = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'email' , [ $email ])]);
2023-08-24 08:23:52 +12:00
if ( $result !== false && ! $result -> isEmpty ()) {
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $result -> getArrayCopy ());
} else {
2021-10-08 08:10:43 +13:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-30 22:44:52 +12:00
if ( $limit !== 0 ) {
2022-05-16 21:58:17 +12:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-08-30 22:44:52 +12:00
2022-02-27 22:57:09 +13:00
if ( $total >= $limit ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-08-30 22:44:52 +12:00
}
}
2023-05-18 13:11:45 +12:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2023-08-24 08:23:52 +12:00
if ( $identityWithMatchingEmail !== false && ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
2023-04-12 11:01:50 +12:00
$userId = $userId === 'unique()' ? ID :: unique () : $userId ;
2021-08-30 22:44:52 +12:00
2023-07-07 12:12:39 +12:00
$user -> setAttributes ([
2022-08-15 02:22:38 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 23:24:31 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2021-12-17 00:30:43 +13:00
'email' => $email ,
'emailVerification' => false ,
'status' => true ,
'password' => null ,
2022-05-05 02:37:37 +12:00
'hash' => Auth :: DEFAULT_ALGO ,
'hashOptions' => Auth :: DEFAULT_ALGO_OPTIONS ,
2022-07-04 21:55:11 +12:00
'passwordUpdate' => null ,
2022-07-14 02:02:49 +12:00
'registration' => DateTime :: now (),
2021-12-17 00:30:43 +13:00
'reset' => false ,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass (),
2022-04-26 22:36:49 +12:00
'sessions' => null ,
2022-04-27 23:06:53 +12:00
'tokens' => null ,
2022-04-28 00:44:47 +12:00
'memberships' => null ,
2023-08-24 08:23:52 +12:00
'search' => implode ( ' ' , [ $userId , $email ])
2023-07-07 12:12:39 +12:00
]);
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2021-08-30 22:44:52 +12:00
}
$loginSecret = Auth :: tokenGenerator ();
2023-05-23 00:22:25 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), Auth :: TOKEN_EXPIRATION_CONFIRM ));
2021-10-08 08:10:43 +13:00
2021-08-30 22:44:52 +12:00
$token = new Document ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2021-08-30 22:44:52 +12:00
'userId' => $user -> getId (),
2022-06-07 21:37:09 +12:00
'userInternalId' => $user -> getInternalId (),
2021-08-30 22:44:52 +12:00
'type' => Auth :: TOKEN_TYPE_MAGIC_URL ,
'secret' => Auth :: hash ( $loginSecret ), // One way hash encryption to protect DB leak
2022-11-05 03:48:29 +13:00
'expire' => $expire ,
2021-08-30 22:44:52 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2021-08-30 22:44:52 +12:00
2022-04-27 23:06:53 +12:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2021-08-30 22:44:52 +12:00
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-08-30 22:44:52 +12:00
2022-05-24 02:54:50 +12:00
if ( empty ( $url )) {
2023-08-24 08:23:52 +12:00
$url = $request -> getProtocol () . '://' . $request -> getHostname () . '/auth/magic-url' ;
2021-08-31 21:22:48 +12:00
}
2021-08-30 22:44:52 +12:00
$url = Template :: parseURL ( $url );
2021-08-31 21:22:48 +12:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getId (), 'secret' => $loginSecret , 'expire' => $expire , 'project' => $project -> getId ()]);
2021-08-30 22:44:52 +12:00
$url = Template :: unParseURL ( $url );
2022-12-14 19:35:04 +13:00
$from = $project -> isEmpty () || $project -> getId () === 'console' ? '' : \sprintf ( $locale -> getText ( 'emails.sender' ), $project -> getAttribute ( 'name' ));
2023-08-24 08:23:52 +12:00
$body = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-base.tpl' );
$subject = $locale -> getText ( " emails.magicSession.subject " );
2022-12-14 19:35:04 +13:00
2023-04-19 20:29:29 +12:00
$smtpEnabled = $project -> getAttribute ( 'smtp' , [])[ 'enabled' ] ? ? false ;
2023-08-24 08:23:52 +12:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.magicSession-' . $locale -> default ] ? ? [];
if ( $smtpEnabled && ! empty ( $customTemplate )) {
2023-07-18 19:05:09 +12:00
$body = $customTemplate [ 'message' ] ? ? $body ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
$from = $customTemplate [ 'senderName' ] ? ? $from ;
2023-04-19 20:29:29 +12:00
}
2022-12-14 19:35:04 +13:00
$body
-> setParam ( '{{subject}}' , $subject )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.magicSession.hello " ))
2022-12-14 19:35:04 +13:00
-> setParam ( '{{name}}' , '' )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{body}}' , $locale -> getText ( " emails.magicSession.body " ))
2022-12-14 19:35:04 +13:00
-> setParam ( '{{redirect}}' , $url )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.magicSession.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.magicSession.thanks " ))
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.magicSession.signature " ))
2022-12-14 19:35:04 +13:00
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' ))
-> setParam ( '{{direction}}' , $locale -> getText ( 'settings.direction' ))
-> setParam ( '{{bg-body}}' , '#f7f7f7' )
-> setParam ( '{{bg-content}}' , '#ffffff' )
-> setParam ( '{{text-content}}' , '#000000' );
$body = $body -> render ();
2021-08-30 22:44:52 +12:00
$mails
2022-12-14 19:35:04 +13:00
-> setSubject ( $subject )
-> setBody ( $body )
-> setFrom ( $from )
2022-04-14 00:39:31 +12:00
-> setRecipient ( $user -> getAttribute ( 'email' ))
2023-08-24 08:23:52 +12:00
-> trigger ()
;
2021-08-30 22:44:52 +12:00
2022-05-11 01:52:55 +12:00
$events -> setPayload (
$response -> output (
2022-04-19 04:21:45 +12:00
$token -> setAttribute ( 'secret' , $loginSecret ),
2021-08-30 22:44:52 +12:00
Response :: MODEL_TOKEN
2022-05-11 01:52:55 +12:00
)
);
2021-08-30 22:44:52 +12:00
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$token -> setAttribute ( 'secret' , ( $isPrivilegedUser || $isAppUser ) ? $loginSecret : '' );
2021-08-30 22:44:52 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2023-08-24 08:23:52 +12:00
-> dynamic ( $token , Response :: MODEL_TOKEN )
;
2021-08-30 22:44:52 +12:00
});
2021-08-31 19:13:30 +12:00
App :: put ( '/v1/account/sessions/magic-url' )
-> desc ( 'Create Magic URL session (confirmation)' )
2022-12-11 21:33:52 +13:00
-> groups ([ 'api' , 'account' , 'session' ])
2021-08-30 22:44:52 +12:00
-> label ( 'scope' , 'public' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.update' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-12 23:01:12 +12:00
-> label ( 'audits.userId' , '{response.userId}' )
2021-08-30 22:44:52 +12:00
-> label ( 'sdk.auth' , [])
-> label ( 'sdk.namespace' , 'account' )
2021-08-31 19:13:30 +12:00
-> label ( 'sdk.method' , 'updateMagicURLSession' )
-> label ( 'sdk.description' , '/docs/references/account/update-magic-url-session.md' )
2021-08-30 22:44:52 +12:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2021-12-11 01:27:11 +13:00
-> param ( 'userId' , '' , new CustomId (), 'User ID.' )
2021-08-30 22:44:52 +12:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-11-01 03:54:15 +13:00
-> inject ( 'project' )
2021-08-30 22:44:52 +12:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $userId , string $secret , Request $request , Response $response , Document $user , Database $dbForProject , Document $project , Locale $locale , Reader $geodb , Event $events ) {
2023-08-24 08:23:52 +12:00
2022-06-08 21:20:23 +12:00
/** @var Utopia\Database\Document $user */
2023-08-24 08:23:52 +12:00
$userFromRequest = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2021-08-30 22:44:52 +12:00
2023-07-07 12:12:39 +12:00
if ( $userFromRequest -> isEmpty ()) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2021-08-30 22:44:52 +12:00
}
2023-07-07 12:12:39 +12:00
$token = Auth :: tokenVerify ( $userFromRequest -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_MAGIC_URL , $secret );
2021-08-30 22:44:52 +12:00
2023-08-24 08:23:52 +12:00
if ( ! $token ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2021-08-30 22:44:52 +12:00
}
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $userFromRequest -> getArrayCopy ());
2022-11-14 22:42:18 +13:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2021-08-30 22:44:52 +12:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
$secret = Auth :: tokenGenerator ();
2023-05-23 00:22:25 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2022-07-05 22:59:03 +12:00
2021-08-30 22:44:52 +12:00
$session = new Document ( array_merge (
[
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2021-10-08 08:10:43 +13:00
'userId' => $user -> getId (),
2022-06-07 21:37:09 +12:00
'userInternalId' => $user -> getInternalId (),
2021-08-30 22:44:52 +12:00
'provider' => Auth :: SESSION_PROVIDER_MAGIC_URL ,
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
));
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2021-08-30 22:44:52 +12:00
2021-12-28 01:45:23 +13:00
$session = $dbForProject -> createDocument ( 'sessions' , $session
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2021-08-30 22:44:52 +12:00
2022-04-04 21:59:32 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-10-08 08:10:43 +13:00
$tokens = $user -> getAttribute ( 'tokens' , []);
/**
* We act like we ' re updating and validating
* the recovery token but actually we don ' t need it anymore .
*/
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteDocument ( 'tokens' , $token );
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-08-30 22:44:52 +12:00
2022-05-09 19:12:06 +12:00
$user -> setAttribute ( 'emailVerification' , true );
2021-08-30 22:44:52 +12:00
2023-07-07 12:12:39 +12:00
try {
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
} catch ( \Throwable $th ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed saving user to DB' );
2021-08-30 22:44:52 +12:00
}
2021-09-01 19:29:12 +12:00
2022-04-04 18:30:07 +12:00
$events
2021-08-30 22:44:52 +12:00
-> setParam ( 'userId' , $user -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'sessionId' , $session -> getId ())
;
2021-08-30 22:44:52 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-04-19 21:30:42 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]));
2021-08-30 22:44:52 +12:00
}
$protocol = $request -> getProtocol ();
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2022-07-13 01:32:39 +12:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2023-08-24 08:23:52 +12:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
;
2021-08-30 22:44:52 +12:00
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-08-30 22:44:52 +12:00
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , $countryName )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'expire' , $expire )
;
2021-08-30 22:44:52 +12:00
$response -> dynamic ( $session , Response :: MODEL_SESSION );
});
2022-06-08 21:00:38 +12:00
App :: post ( '/v1/account/sessions/phone' )
-> desc ( 'Create Phone session' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'public' )
-> label ( 'auth.type' , 'phone' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2022-06-08 21:00:38 +12:00
-> 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 )
2023-06-16 22:42:24 +12:00
-> label ( 'abuse-key' , 'url:{url},email:{param-phone}' )
2023-01-21 11:22:16 +13:00
-> param ( 'userId' , '' , new CustomId (), 'Unique 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.' )
2022-08-15 03:10:12 +12:00
-> param ( 'phone' , '' , new Phone (), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.' )
2022-06-08 21:00:38 +12:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2022-06-08 21:00:38 +12:00
-> inject ( 'project' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2022-06-09 01:57:34 +12:00
-> inject ( 'messaging' )
2023-04-19 20:29:29 +12:00
-> inject ( 'locale' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $userId , string $phone , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Event $events , EventPhone $messaging , Locale $locale ) {
2023-08-24 08:23:52 +12:00
2022-08-17 19:32:42 +12:00
if ( empty ( App :: getEnv ( '_APP_SMS_PROVIDER' ))) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: GENERAL_PHONE_DISABLED , 'Phone provider not configured' );
2022-06-08 21:00:38 +12:00
}
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2023-07-07 12:12:39 +12:00
$result = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'phone' , [ $phone ])]);
2023-08-24 08:23:52 +12:00
if ( $result !== false && ! $result -> isEmpty ()) {
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $result -> getArrayCopy ());
} else {
2022-06-08 21:00:38 +12:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
if ( $limit !== 0 ) {
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
if ( $total >= $limit ) {
2022-08-15 19:13:24 +12:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2022-06-08 21:00:38 +12:00
}
}
2022-08-15 02:22:38 +12:00
$userId = $userId == 'unique()' ? ID :: unique () : $userId ;
2023-07-07 12:12:39 +12:00
$user -> setAttributes ([
2022-08-15 02:22:38 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 23:24:31 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2022-06-08 21:00:38 +12:00
'email' => null ,
2022-08-15 01:43:41 +12:00
'phone' => $phone ,
2022-06-08 21:00:38 +12:00
'emailVerification' => false ,
'phoneVerification' => false ,
'status' => true ,
'password' => null ,
2022-07-04 21:55:11 +12:00
'passwordUpdate' => null ,
2022-07-14 02:02:49 +12:00
'registration' => DateTime :: now (),
2022-06-08 21:00:38 +12:00
'reset' => false ,
'prefs' => new \stdClass (),
'sessions' => null ,
'tokens' => null ,
'memberships' => null ,
2023-08-24 08:23:52 +12:00
'search' => implode ( ' ' , [ $userId , $phone ])
2023-07-07 12:12:39 +12:00
]);
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2022-06-08 21:00:38 +12:00
}
2022-09-19 20:09:48 +12:00
$secret = Auth :: codeGenerator ();
2023-05-23 00:48:52 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), Auth :: TOKEN_EXPIRATION_PHONE ));
2022-06-08 21:00:38 +12:00
$token = new Document ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2022-06-08 21:00:38 +12:00
'userId' => $user -> getId (),
2022-06-21 09:38:45 +12:00
'userInternalId' => $user -> getInternalId (),
2022-06-08 21:00:38 +12:00
'type' => Auth :: TOKEN_TYPE_PHONE ,
2022-09-23 10:25:17 +12:00
'secret' => Auth :: hash ( $secret ),
2022-06-08 21:00:38 +12:00
'expire' => $expire ,
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2022-06-08 21:00:38 +12:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2022-06-08 21:00:38 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2023-08-24 08:23:52 +12:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/sms-base.tpl' );
2023-04-19 20:44:22 +12:00
2023-08-24 08:23:52 +12:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'sms.login-' . $locale -> default ] ? ? [];
if ( ! empty ( $customTemplate )) {
2023-07-18 19:05:09 +12:00
$message = $customTemplate [ 'message' ] ? ? $message ;
2023-04-19 20:29:29 +12:00
}
2023-03-17 13:55:00 +13:00
$message = $message -> setParam ( '{{token}}' , $secret );
2023-03-14 22:07:42 +13:00
$message = $message -> render ();
2022-06-09 01:57:34 +12:00
$messaging
2022-08-15 01:43:41 +12:00
-> setRecipient ( $phone )
2023-03-14 22:07:42 +13:00
-> setMessage ( $message )
2022-06-09 01:57:34 +12:00
-> trigger ();
2022-06-08 21:00:38 +12:00
$events -> setPayload (
$response -> output (
$token -> setAttribute ( 'secret' , $secret ),
Response :: MODEL_TOKEN
)
);
// Hide secret for clients
$token -> setAttribute ( 'secret' , ( $isPrivilegedUser || $isAppUser ) ? $secret : '' );
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2023-08-24 08:23:52 +12:00
-> dynamic ( $token , Response :: MODEL_TOKEN )
;
2022-06-08 21:00:38 +12:00
});
App :: put ( '/v1/account/sessions/phone' )
2022-08-17 01:20:35 +12:00
-> desc ( 'Create Phone Session (confirmation)' )
2022-12-11 21:33:52 +13:00
-> groups ([ 'api' , 'account' , 'session' ])
2022-06-08 21:00:38 +12:00
-> 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' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2022-06-08 21:00:38 +12:00
-> inject ( 'dbForProject' )
2022-11-01 03:54:15 +13:00
-> inject ( 'project' )
2022-06-08 21:00:38 +12:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
-> inject ( 'events' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $userId , string $secret , Request $request , Response $response , Document $user , Database $dbForProject , Document $project , Locale $locale , Reader $geodb , Event $events ) {
2023-08-24 08:23:52 +12:00
$userFromRequest = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2022-06-08 21:00:38 +12:00
2023-07-07 12:12:39 +12:00
if ( $userFromRequest -> isEmpty ()) {
2022-08-15 19:54:54 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2022-06-08 21:00:38 +12:00
}
2023-07-07 12:12:39 +12:00
$token = Auth :: phoneTokenVerify ( $userFromRequest -> getAttribute ( 'tokens' , []), $secret );
2022-06-08 21:00:38 +12:00
2023-08-24 08:23:52 +12:00
if ( ! $token ) {
2022-08-15 19:54:54 +12:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2022-06-08 21:00:38 +12:00
}
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $userFromRequest -> getArrayCopy ());
2022-11-14 22:42:18 +13:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-06-08 21:00:38 +12:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
$secret = Auth :: tokenGenerator ();
2023-05-23 00:48:52 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2022-07-05 22:59:03 +12:00
2022-06-08 21:00:38 +12:00
$session = new Document ( array_merge (
[
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2022-06-08 21:00:38 +12:00
'userId' => $user -> getId (),
2022-06-21 09:38:45 +12:00
'userInternalId' => $user -> getInternalId (),
2022-06-08 21:00:38 +12:00
'provider' => Auth :: SESSION_PROVIDER_PHONE ,
'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 ()
));
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2022-06-08 21:00:38 +12:00
$session = $dbForProject -> createDocument ( 'sessions' , $session
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2022-06-08 21:00:38 +12:00
$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 );
2023-07-07 12:12:39 +12:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-06-08 21:00:38 +12:00
if ( false === $user ) {
2022-08-15 19:54:54 +12:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed saving user to DB' );
2022-06-08 21:00:38 +12:00
}
$events
-> setParam ( 'userId' , $user -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'sessionId' , $session -> getId ())
;
2022-06-08 21:00:38 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-06-08 21:00:38 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]));
}
$protocol = $request -> getProtocol ();
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2022-07-13 01:32:39 +12:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2023-08-24 08:23:52 +12:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
;
2022-06-08 21:00:38 +12:00
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2022-06-08 21:00:38 +12:00
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , $countryName )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'expire' , $expire )
;
2022-06-08 21:00:38 +12:00
$response -> dynamic ( $session , Response :: MODEL_SESSION );
});
2021-02-17 02:46:30 +13:00
App :: post ( '/v1/account/sessions/anonymous' )
2021-02-19 03:52:27 +13:00
-> desc ( 'Create Anonymous Session' )
2022-12-11 21:33:52 +13:00
-> groups ([ 'api' , 'account' , 'auth' , 'session' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2021-02-17 02:46:30 +13:00
-> label ( 'scope' , 'public' )
2021-04-03 21:56:32 +13:00
-> label ( 'auth.type' , 'anonymous' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-16 21:00:28 +12:00
-> label ( 'audits.userId' , '{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [])
2021-02-17 02:46:30 +13:00
-> label ( 'sdk.namespace' , 'account' )
2021-02-19 03:52:27 +13:00
-> label ( 'sdk.method' , 'createAnonymousSession' )
-> label ( 'sdk.description' , '/docs/references/account/create-session-anonymous.md' )
2021-02-17 02:46:30 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION )
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'locale' )
-> inject ( 'user' )
2021-02-24 11:43:05 +13:00
-> inject ( 'project' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2021-02-17 02:46:30 +13:00
-> inject ( 'geodb' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2022-08-10 20:45:10 +12:00
-> action ( function ( Request $request , Response $response , Locale $locale , Document $user , Document $project , Database $dbForProject , Reader $geodb , Event $events ) {
2023-08-24 08:23:52 +12:00
2021-02-17 02:46:30 +13:00
$protocol = $request -> getProtocol ();
2021-04-22 02:54:05 +12:00
if ( 'console' === $project -> getId ()) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_ANONYMOUS_CONSOLE_PROHIBITED , 'Failed to create anonymous user' );
2021-02-17 02:46:30 +13:00
}
2023-08-24 08:23:52 +12:00
if ( ! $user -> isEmpty ()) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_SESSION_ALREADY_EXISTS , 'Cannot create an anonymous user when logged in' );
2021-04-22 02:54:05 +12:00
}
2021-08-06 20:34:17 +12:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-04-03 21:56:32 +13:00
if ( $limit !== 0 ) {
2022-05-16 21:58:17 +12:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-04-03 21:56:32 +13:00
2022-02-27 22:57:09 +13:00
if ( $total >= $limit ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-04-03 21:56:32 +13:00
}
}
2022-08-15 02:22:38 +12:00
$userId = ID :: unique ();
2023-07-07 12:12:39 +12:00
$user -> setAttributes ([
2022-08-15 02:22:38 +12:00
'$id' => $userId ,
2022-08-02 21:21:53 +12:00
'$permissions' => [
2022-08-14 17:21:11 +12:00
Permission :: read ( Role :: any ()),
2022-08-15 02:22:38 +12:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 21:21:53 +12:00
],
2021-05-10 08:34:32 +12:00
'email' => null ,
'emailVerification' => false ,
2021-07-14 23:02:12 +12:00
'status' => true ,
2021-05-10 08:34:32 +12:00
'password' => null ,
2022-05-05 02:37:37 +12:00
'hash' => Auth :: DEFAULT_ALGO ,
'hashOptions' => Auth :: DEFAULT_ALGO_OPTIONS ,
2022-07-04 21:55:11 +12:00
'passwordUpdate' => null ,
2022-07-14 02:02:49 +12:00
'registration' => DateTime :: now (),
2021-05-10 08:34:32 +12:00
'reset' => false ,
'name' => null ,
2021-12-28 23:48:50 +13:00
'prefs' => new \stdClass (),
2022-04-26 22:36:49 +12:00
'sessions' => null ,
2022-04-27 23:06:53 +12:00
'tokens' => null ,
2022-04-28 00:44:47 +12:00
'memberships' => null ,
2023-07-07 12:12:39 +12:00
'search' => $userId ,
]);
2023-08-24 08:23:52 +12:00
Authorization :: skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2021-02-17 02:46:30 +13:00
2021-02-17 03:16:09 +13:00
// Create session token
2022-11-14 22:42:18 +13:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2021-02-17 02:46:30 +13:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
$secret = Auth :: tokenGenerator ();
2023-05-23 00:48:52 +12:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2022-07-05 22:59:03 +12:00
2021-02-17 02:46:30 +13:00
$session = new Document ( array_merge (
[
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2021-06-13 08:44:25 +12:00
'userId' => $user -> getId (),
2022-06-07 21:37:09 +12:00
'userInternalId' => $user -> getInternalId (),
2021-03-29 22:16:56 +13:00
'provider' => Auth :: SESSION_PROVIDER_ANONYMOUS ,
2021-02-17 02:46:30 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
));
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2021-02-17 02:46:30 +13:00
2023-08-24 08:23:52 +12:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
]));
2021-07-17 22:04:43 +12:00
2022-04-04 21:59:32 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-02-17 02:46:30 +13:00
2022-04-04 18:30:07 +12:00
$events
-> setParam ( 'userId' , $user -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'sessionId' , $session -> getId ())
;
2022-04-04 18:30:07 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-04-19 21:30:42 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]));
2021-02-17 02:46:30 +13:00
}
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2022-07-13 01:32:39 +12:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2023-08-24 08:23:52 +12:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
;
2021-02-17 02:46:30 +13:00
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-06-17 21:33:57 +12:00
2021-02-17 02:46:30 +13:00
$session
-> setAttribute ( 'current' , true )
2021-06-17 21:33:57 +12:00
-> setAttribute ( 'countryName' , $countryName )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'expire' , $expire )
;
2021-02-17 02:46:30 +13:00
2021-07-26 02:47:18 +12:00
$response -> dynamic ( $session , Response :: MODEL_SESSION );
2021-02-17 02:46:30 +13:00
});
2020-12-29 09:31:55 +13:00
App :: post ( '/v1/account/jwt' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Create JWT' )
2021-03-01 07:36:13 +13:00
-> groups ([ 'api' , 'account' , 'auth' ])
2020-10-18 06:49:09 +13:00
-> label ( 'scope' , 'account' )
2021-03-01 07:36:13 +13:00
-> label ( 'auth.type' , 'jwt' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION ])
2020-10-18 06:49:09 +13:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'createJWT' )
-> label ( 'sdk.description' , '/docs/references/account/create-jwt.md' )
2021-05-20 20:52:19 +12:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_JWT )
2022-06-07 08:57:37 +12:00
-> label ( 'abuse-limit' , 100 )
2021-07-27 23:07:39 +12:00
-> label ( 'abuse-key' , 'url:{url},userId:{userId}' )
2020-12-29 06:03:27 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2022-04-04 21:59:32 +12:00
-> inject ( 'dbForProject' )
2022-05-19 04:14:21 +12:00
-> action ( function ( Response $response , Document $user , Database $dbForProject ) {
2023-08-24 08:23:52 +12:00
2022-04-26 20:52:59 +12:00
$sessions = $user -> getAttribute ( 'sessions' , []);
$current = new Document ();
foreach ( $sessions as $session ) { /** @var Utopia\Database\Document $session */
if ( $session -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
$current = $session ;
}
}
if ( $current -> isEmpty ()) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2020-12-29 06:03:27 +13:00
}
2021-08-05 17:06:38 +12:00
2020-12-29 10:23:09 +13:00
$jwt = new JWT ( App :: getEnv ( '_APP_OPENSSL_KEY_V1' ), 'HS256' , 900 , 10 ); // Instantiate with key, algo, maxAge and leeway.
2020-12-29 06:03:27 +13:00
2022-09-07 23:02:36 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( new Document ([ 'jwt' => $jwt -> encode ([
2023-08-24 08:23:52 +12:00
// 'uid' => 1,
// 'aud' => 'http://site.com',
// 'scopes' => ['user'],
// 'iss' => 'http://api.mysite.com',
'userId' => $user -> getId (),
'sessionId' => $current -> getId (),
])]), Response :: MODEL_JWT );
2020-12-29 06:03:27 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account' )
2020-02-01 11:34:07 +13:00
-> desc ( 'Get Account' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-02-01 11:34:07 +13:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'get' )
-> label ( 'sdk.description' , '/docs/references/account/get.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account' )
-> label ( 'sdk.offline.key' , 'current' )
2020-12-27 07:11:18 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2022-08-10 20:45:10 +12:00
-> action ( function ( Response $response , Document $user ) {
2023-08-24 08:23:52 +12:00
2022-05-06 20:58:36 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/prefs' )
2020-02-01 11:34:07 +13:00
-> desc ( 'Get Account Preferences' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-02-01 11:34:07 +13:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'getPrefs' )
-> label ( 'sdk.description' , '/docs/references/account/get-prefs.md' )
2020-11-13 00:54:16 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2021-04-22 01:37:51 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_PREFERENCES )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account/prefs' )
-> label ( 'sdk.offline.key' , 'current' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2022-08-10 20:45:10 +12:00
-> action ( function ( Response $response , Document $user ) {
2023-08-24 08:23:52 +12:00
2023-04-13 04:02:43 +12:00
$prefs = $user -> getAttribute ( 'prefs' , []);
2020-06-30 09:43:34 +12:00
2021-07-26 02:47:18 +12:00
$response -> dynamic ( new Document ( $prefs ), Response :: MODEL_PREFERENCES );
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/sessions' )
2022-11-04 04:24:32 +13:00
-> desc ( 'List Sessions' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-02-01 11:34:07 +13:00
-> label ( 'sdk.namespace' , 'account' )
2022-09-14 10:13:04 +12:00
-> label ( 'sdk.method' , 'listSessions' )
-> label ( 'sdk.description' , '/docs/references/account/list-sessions.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION_LIST )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account/sessions' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'locale' )
2022-11-04 22:50:59 +13:00
-> inject ( 'project' )
-> action ( function ( Response $response , Document $user , Locale $locale , Document $project ) {
2023-08-24 08:23:52 +12:00
2021-02-20 01:12:47 +13:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-11-14 22:30:45 +13:00
$current = Auth :: sessionVerify ( $sessions , Auth :: $secret , $authDuration );
2020-06-30 09:43:34 +12:00
2021-08-05 17:06:38 +12:00
foreach ( $sessions as $key => $session ) { /** @var Document $session */
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-06-17 21:08:01 +12:00
$session -> setAttribute ( 'countryName' , $countryName );
2021-02-20 01:12:47 +13:00
$session -> setAttribute ( 'current' , ( $current == $session -> getId ()) ? true : false );
2023-05-21 03:39:48 +12:00
$session -> setAttribute ( 'expire' , DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime ( $session -> getCreatedAt ()), $authDuration )));
2020-02-01 11:34:07 +13:00
2021-02-20 01:12:47 +13:00
$sessions [ $key ] = $session ;
2020-02-01 11:34:07 +13:00
}
2020-06-30 09:43:34 +12:00
2021-07-26 02:47:18 +12:00
$response -> dynamic ( new Document ([
2021-05-27 22:09:14 +12:00
'sessions' => $sessions ,
2022-02-27 22:57:09 +13:00
'total' => count ( $sessions ),
2020-10-31 08:53:27 +13:00
]), Response :: MODEL_SESSION_LIST );
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2020-06-29 05:31:21 +12:00
App :: get ( '/v1/account/logs' )
2022-11-04 04:24:32 +13:00
-> desc ( 'List Logs' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-02-01 11:34:07 +13:00
-> label ( 'sdk.namespace' , 'account' )
2022-09-14 09:05:03 +12:00
-> label ( 'sdk.method' , 'listLogs' )
-> label ( 'sdk.description' , '/docs/references/account/list-logs.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_LOG_LIST )
2023-05-17 00:56:20 +12:00
-> param ( 'queries' , [], new Queries ([ new Limit (), new Offset ()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'locale' )
-> inject ( 'geodb' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-08-25 21:59:28 +12:00
-> action ( function ( array $queries , Response $response , Document $user , Locale $locale , Reader $geodb , Database $dbForProject ) {
2023-08-24 08:23:52 +12:00
2022-08-24 01:06:59 +12:00
$queries = Query :: parseQueries ( $queries );
$grouped = Query :: groupByType ( $queries );
2022-08-30 23:55:23 +12:00
$limit = $grouped [ 'limit' ] ? ? APP_LIMIT_COUNT ;
2022-08-24 01:06:59 +12:00
$offset = $grouped [ 'offset' ] ? ? 0 ;
2022-08-24 01:10:27 +12:00
2022-05-26 01:49:32 +12:00
$audit = new EventAudit ( $dbForProject );
2022-04-04 18:30:07 +12:00
2023-07-13 05:56:24 +12:00
$logs = $audit -> getLogsByUser ( $user -> getInternalId (), $limit , $offset );
2020-06-30 09:43:34 +12:00
$output = [];
foreach ( $logs as $i => & $log ) {
2023-08-24 08:23:52 +12:00
$log [ 'userAgent' ] = ( ! empty ( $log [ 'userAgent' ])) ? $log [ 'userAgent' ] : 'UNKNOWN' ;
2020-06-30 09:43:34 +12:00
2021-02-15 06:28:54 +13:00
$detector = new Detector ( $log [ 'userAgent' ]);
2020-06-30 09:43:34 +12:00
2021-11-18 23:33:42 +13:00
$output [ $i ] = new Document ( array_merge (
$log -> getArrayCopy (),
$log [ 'data' ],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
));
2020-10-31 08:53:27 +13:00
$record = $geodb -> get ( $log [ 'ip' ]);
if ( $record ) {
2023-08-24 08:23:52 +12:00
$output [ $i ][ 'countryCode' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), false ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), $locale -> getText ( 'locale.country.unknown' ));
2020-10-31 08:53:27 +13:00
} else {
$output [ $i ][ 'countryCode' ] = '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'locale.country.unknown' );
2020-02-01 11:34:07 +13:00
}
}
2020-06-30 09:43:34 +12:00
2021-11-17 03:54:29 +13:00
$response -> dynamic ( new Document ([
2022-04-04 18:30:07 +12:00
'total' => $audit -> countLogsByUser ( $user -> getId ()),
2021-11-17 03:54:29 +13:00
'logs' => $output ,
]), Response :: MODEL_LOG_LIST );
2020-12-27 03:31:53 +13:00
});
2020-02-01 11:34:07 +13:00
2023-08-17 02:43:38 +12:00
App :: get ( '/v1/account/targets' )
-> desc ( 'List Account Targets' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'listTargets' )
-> label ( 'sdk.description' , '/docs/references/account/list-targets.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TARGET_LIST )
-> inject ( 'user' )
2023-08-24 08:24:25 +12:00
-> inject ( 'response' )
-> action ( function ( Document $user , Response $response ) {
2023-08-24 08:23:52 +12:00
2023-08-17 02:43:38 +12:00
$targets = $user -> getAttribute ( 'targets' , []);
$response -> dynamic ( new Document ([
'targets' => $targets ,
2023-08-17 23:54:45 +12:00
'total' => \count ( $targets ),
2023-08-17 02:43:38 +12:00
]), Response :: MODEL_TARGET_LIST );
});
2021-06-16 22:14:08 +12:00
App :: get ( '/v1/account/sessions/:sessionId' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Get Session' )
2021-06-16 22:14:08 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
2021-06-16 22:48:12 +12:00
-> label ( 'sdk.method' , 'getSession' )
-> label ( 'sdk.description' , '/docs/references/account/get-session.md' )
2021-06-16 22:14:08 +12:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2021-06-16 22:48:12 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account/sessions' )
-> label ( 'sdk.offline.key' , '{sessionId}' )
2022-09-19 22:05:42 +12:00
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to get the current device session.' )
2021-06-16 22:14:08 +12:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'locale' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-11-04 04:03:39 +13:00
-> inject ( 'project' )
-> action ( function ( ? string $sessionId , Response $response , Document $user , Locale $locale , Database $dbForProject , Document $project ) {
2023-08-24 08:23:52 +12:00
2021-08-05 17:06:38 +12:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2021-08-05 17:06:38 +12:00
$sessionId = ( $sessionId === 'current' )
2022-11-14 22:30:45 +13:00
? Auth :: sessionVerify ( $user -> getAttribute ( 'sessions' ), Auth :: $secret , $authDuration )
2022-04-26 00:59:08 +12:00
: $sessionId ;
2021-06-16 22:14:08 +12:00
2021-08-05 17:06:38 +12:00
foreach ( $sessions as $session ) { /** @var Document $session */
2021-07-05 02:14:06 +12:00
if ( $sessionId == $session -> getId ()) {
2023-08-24 08:23:52 +12:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-06-17 21:33:57 +12:00
2021-07-05 02:14:06 +12:00
$session
-> setAttribute ( 'current' , ( $session -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )))
-> setAttribute ( 'countryName' , $countryName )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'expire' , DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime ( $session -> getCreatedAt ()), $authDuration )))
;
2021-08-05 17:06:38 +12:00
2021-07-26 02:47:18 +12:00
return $response -> dynamic ( $session , Response :: MODEL_SESSION );
2021-07-05 02:14:06 +12:00
}
}
2021-06-16 22:48:12 +12:00
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2021-06-16 22:14:08 +12:00
});
2023-08-17 02:43:38 +12:00
App :: get ( '/v1/account/targets/:targetId' )
-> desc ( 'Get Target' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'getTarget' )
-> label ( 'sdk.description' , '/docs/references/account/get-Target.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-08-24 08:24:25 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_TARGET )
2023-08-17 02:43:38 +12:00
-> label ( 'sdk.offline.model' , '/account/targets' )
-> label ( 'sdk.offline.key' , '{targetId}' )
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
-> inject ( 'user' )
2023-08-24 08:24:25 +12:00
-> inject ( 'response' )
-> action ( function ( string $targetId , Document $user , Response $response ) {
2023-08-24 08:23:52 +12:00
2023-08-17 02:43:38 +12:00
$target = $user -> find ( '$id' , $targetId , 'targets' );
if ( empty ( $target )) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
}
$response -> dynamic ( $target , Response :: MODEL_TARGET );
});
2020-06-29 05:31:21 +12:00
App :: patch ( '/v1/account/name' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Name' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].update.name' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-17 19:05:37 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'updateName' )
2020-01-06 12:22:02 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-name.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account' )
-> label ( 'sdk.offline.key' , 'current' )
2020-09-11 02:40:14 +12:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-01-20 13:36:17 +13:00
-> action ( function ( string $name , ? \DateTime $requestTimestamp , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
2023-05-31 08:55:33 +12:00
$user -> setAttribute ( 'name' , $name );
2023-01-20 13:36:17 +13:00
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2020-06-30 09:43:34 +12:00
2022-04-14 00:39:31 +12:00
$events -> setParam ( 'userId' , $user -> getId ());
2021-08-16 20:53:34 +12:00
2022-05-06 20:58:36 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App :: patch ( '/v1/account/password' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Password' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].update.password' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-08-17 02:56:05 +12:00
-> label ( 'audits.userId' , '{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'updatePassword' )
2020-01-06 12:22:02 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-password.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-04-11 20:41:24 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account' )
-> label ( 'sdk.offline.key' , 'current' )
2023-03-09 14:21:24 +13:00
-> param ( 'password' , '' , fn ( $project , $passwordsDictionary ) => new PasswordDictionary ( $passwordsDictionary , $project -> getAttribute ( 'auths' , [])[ 'passwordDictionary' ] ? ? false ), 'New user password. Must be at least 8 chars.' , false , [ 'project' , 'passwordsDictionary' ])
2021-12-11 01:27:11 +13:00
-> param ( 'oldPassword' , '' , new Password (), 'Current user password. Must be at least 8 chars.' , true )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2022-12-16 23:47:08 +13:00
-> inject ( 'project' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-01-20 13:36:17 +13:00
-> action ( function ( string $password , string $oldPassword , ? \DateTime $requestTimestamp , Response $response , Document $user , Document $project , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
2021-05-05 06:52:46 +12:00
// Check old password only if its an existing user.
2023-08-24 08:23:52 +12:00
if ( ! empty ( $user -> getAttribute ( 'passwordUpdate' )) && ! Auth :: passwordVerify ( $oldPassword , $user -> getAttribute ( 'password' ), $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ))) { // Double check user password
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-30 09:43:34 +12:00
}
2019-05-09 18:54:39 +12:00
2022-12-26 18:52:49 +13:00
$newPassword = Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS );
2022-12-18 19:27:41 +13:00
$historyLimit = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
2023-07-20 09:34:27 +12:00
$history = $user -> getAttribute ( 'passwordHistory' , []);
2022-12-18 19:31:14 +13:00
if ( $historyLimit > 0 ) {
2022-12-18 22:08:51 +13:00
$validator = new PasswordHistory ( $history , $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ));
2023-08-24 08:23:52 +12:00
if ( ! $validator -> isValid ( $password )) {
2023-04-14 08:20:03 +12:00
throw new Exception ( Exception :: USER_PASSWORD_RECENTLY_USED );
2022-12-16 23:47:08 +13:00
}
2022-12-18 19:27:41 +13:00
2022-12-16 23:47:08 +13:00
$history [] = $newPassword ;
2023-07-20 09:34:27 +12:00
$history = array_slice ( $history , ( count ( $history ) - $historyLimit ), $historyLimit );
2022-12-16 23:22:39 +13:00
}
2023-07-20 10:24:32 +12:00
if ( $project -> getAttribute ( 'auths' , [])[ 'personalDataCheck' ] ? ? false ) {
2023-04-14 08:20:03 +12:00
$personalDataValidator = new PersonalData ( $user -> getId (), $user -> getAttribute ( 'email' ), $user -> getAttribute ( 'name' ), $user -> getAttribute ( 'phone' ));
2023-08-24 08:23:52 +12:00
if ( ! $personalDataValidator -> isValid ( $password )) {
2023-04-14 08:20:03 +12:00
throw new Exception ( Exception :: USER_PASSWORD_PERSONAL_DATA );
}
2022-12-16 23:22:39 +13:00
}
2023-01-20 13:36:17 +13:00
$user
-> setAttribute ( 'password' , $newPassword )
-> setAttribute ( 'passwordHistory' , $history )
-> setAttribute ( 'passwordUpdate' , DateTime :: now ())
-> setAttribute ( 'hash' , Auth :: DEFAULT_ALGO )
-> setAttribute ( 'hashOptions' , Auth :: DEFAULT_ALGO_OPTIONS );
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2020-06-30 09:43:34 +12:00
2022-04-14 00:39:31 +12:00
$events -> setParam ( 'userId' , $user -> getId ());
2022-04-04 18:30:07 +12:00
2022-05-06 20:58:36 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2020-06-29 05:31:21 +12:00
App :: patch ( '/v1/account/email' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Email' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].update.email' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'updateEmail' )
2019-10-08 20:21:54 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-email.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account' )
-> label ( 'sdk.offline.key' , 'current' )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2021-11-26 09:07:54 +13:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-01-20 13:36:17 +13:00
-> action ( function ( string $email , string $password , ? \DateTime $requestTimestamp , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-05-26 09:16:19 +12:00
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user -> getAttribute ( 'passwordUpdate' );
2021-02-17 02:46:30 +13:00
if (
2023-08-24 08:23:52 +12:00
! empty ( $passwordUpdate ) &&
! Auth :: passwordVerify ( $password , $user -> getAttribute ( 'password' ), $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ))
2021-02-17 02:46:30 +13:00
) { // Double check user password
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-30 09:43:34 +12:00
}
2019-07-21 23:43:06 +12:00
2021-06-04 01:03:51 +12:00
$email = \strtolower ( $email );
2019-05-09 18:54:39 +12:00
2023-05-18 13:11:45 +12:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
Query :: notEqual ( 'userId' , $user -> getId ()),
]);
2023-08-24 08:23:52 +12:00
if ( $identityWithMatchingEmail !== false && ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 13:11:45 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
}
2022-06-09 00:50:31 +12:00
$user
-> setAttribute ( 'email' , $email )
-> setAttribute ( 'emailVerification' , false ) // After this user needs to confirm mail again
2023-08-24 08:23:52 +12:00
;
2019-05-09 18:54:39 +12:00
2023-05-26 09:16:19 +12:00
if ( empty ( $passwordUpdate )) {
$user
-> setAttribute ( 'password' , Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS ))
-> setAttribute ( 'hash' , Auth :: DEFAULT_ALGO )
-> setAttribute ( 'hashOptions' , Auth :: DEFAULT_ALGO_OPTIONS )
-> setAttribute ( 'passwordUpdate' , DateTime :: now ());
}
2021-08-30 00:00:25 +12:00
try {
2023-01-20 13:36:17 +13:00
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2023-06-16 14:16:19 +12:00
} catch ( Duplicate ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
2021-08-30 00:00:25 +12:00
}
2019-05-09 18:54:39 +12:00
2022-04-14 00:39:31 +12:00
$events -> setParam ( 'userId' , $user -> getId ());
2022-04-04 18:30:07 +12:00
2022-05-06 20:58:36 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2022-06-09 00:50:31 +12:00
App :: patch ( '/v1/account/phone' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Phone' )
2022-06-09 00:50:31 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].update.phone' )
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-06-09 00:50:31 +12:00
-> 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 )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account' )
-> label ( 'sdk.offline.key' , 'current' )
2022-08-15 03:10:12 +12:00
-> param ( 'phone' , '' , new Phone (), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.' )
2022-06-09 00:50:31 +12:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2022-06-09 00:50:31 +12:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2023-01-20 13:36:17 +13:00
-> action ( function ( string $phone , string $password , ? \DateTime $requestTimestamp , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-05-26 09:16:19 +12:00
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user -> getAttribute ( 'passwordUpdate' );
2022-06-09 00:50:31 +12:00
if (
2023-08-24 08:23:52 +12:00
! empty ( $passwordUpdate ) &&
! Auth :: passwordVerify ( $password , $user -> getAttribute ( 'password' ), $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ))
2022-06-09 00:50:31 +12:00
) { // Double check user password
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2022-06-09 00:50:31 +12:00
}
$user
-> setAttribute ( 'phone' , $phone )
-> setAttribute ( 'phoneVerification' , false ) // After this user needs to confirm phone number again
2023-08-24 08:23:52 +12:00
;
2022-06-09 00:50:31 +12:00
2023-06-01 08:52:05 +12:00
if ( empty ( $passwordUpdate )) {
$user
-> setAttribute ( 'password' , Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS ))
-> setAttribute ( 'hash' , Auth :: DEFAULT_ALGO )
-> setAttribute ( 'hashOptions' , Auth :: DEFAULT_ALGO_OPTIONS )
-> setAttribute ( 'passwordUpdate' , DateTime :: now ());
}
2022-06-09 00:50:31 +12:00
try {
2023-01-20 13:36:17 +13:00
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2022-06-09 00:50:31 +12:00
} catch ( Duplicate $th ) {
2022-08-16 18:59:03 +12:00
throw new Exception ( Exception :: USER_PHONE_ALREADY_EXISTS );
2022-06-09 00:50:31 +12:00
}
$events -> setParam ( 'userId' , $user -> getId ());
2022-07-26 00:37:29 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2022-06-09 00:50:31 +12:00
});
2020-06-29 05:31:21 +12:00
App :: patch ( '/v1/account/prefs' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Preferences' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].update.prefs' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'updatePrefs' )
2019-10-08 20:21:54 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-prefs.md' )
2020-11-13 00:54:16 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-02-15 13:10:54 +13:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-02-10 14:06:33 +13:00
-> label ( 'sdk.offline.model' , '/account/prefs' )
-> label ( 'sdk.offline.key' , 'current' )
2020-10-31 08:53:27 +13:00
-> param ( 'prefs' , [], new Assoc (), 'Prefs key-value JSON object.' )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-01-20 13:36:17 +13:00
-> action ( function ( array $prefs , ? \DateTime $requestTimestamp , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
2023-01-20 13:36:17 +13:00
$user -> setAttribute ( 'prefs' , $prefs );
2019-05-09 18:54:39 +12:00
2023-01-20 13:36:17 +13:00
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2019-05-09 18:54:39 +12:00
2022-04-14 00:39:31 +12:00
$events -> setParam ( 'userId' , $user -> getId ());
2020-01-12 02:58:02 +13:00
2022-05-06 20:58:36 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2019-05-09 18:54:39 +12:00
2022-05-16 21:58:17 +12:00
App :: patch ( '/v1/account/status' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update Status' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2022-05-16 21:58:17 +12:00
-> label ( 'event' , 'users.[userId].update.status' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2022-09-09 01:06:16 +12:00
-> label ( 'audits.event' , 'user.update' )
2022-08-09 02:32:54 +12:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
2022-05-16 21:58:17 +12:00
-> label ( 'sdk.method' , 'updateStatus' )
-> label ( 'sdk.description' , '/docs/references/account/update-status.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
2023-04-11 20:41:24 +12:00
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2023-05-20 06:58:17 +12:00
-> inject ( 'request' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2020-12-27 03:31:53 +13:00
-> inject ( 'events' )
2023-05-20 06:58:17 +12:00
-> action ( function ( ? \DateTime $requestTimestamp , Request $request , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
2023-01-20 13:36:17 +13:00
$user -> setAttribute ( 'status' , false );
2020-06-30 09:43:34 +12:00
2023-01-20 13:36:17 +13:00
$user = $dbForProject -> withRequestTimestamp ( $requestTimestamp , fn () => $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user ));
2021-05-07 10:31:05 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
-> setParam ( 'userId' , $user -> getId ())
2022-06-10 20:03:09 +12:00
-> setPayload ( $response -> output ( $user , Response :: MODEL_ACCOUNT ));
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-04-14 00:39:31 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]));
2019-05-09 18:54:39 +12:00
}
2020-06-30 09:43:34 +12:00
2023-05-20 06:58:17 +12:00
$protocol = $request -> getProtocol ();
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( Auth :: $cookieName , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
;
2023-05-20 06:58:17 +12:00
2022-06-10 20:03:09 +12:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-06-29 05:31:21 +12:00
App :: delete ( '/v1/account/sessions/:sessionId' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Delete Session' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'account' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].delete' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.delete' )
2022-08-13 01:21:32 +12:00
-> label ( 'audits.resource' , 'user/{user.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'deleteSession' )
2020-01-06 12:07:41 +13:00
-> label ( 'sdk.description' , '/docs/references/account/delete-session.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_NOCONTENT )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 100 )
2022-09-19 22:05:42 +12:00
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to delete the current device session.' )
2023-01-20 13:36:17 +13:00
-> inject ( 'requestTimestamp' )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2021-06-13 08:44:25 +12:00
-> inject ( 'locale' )
2020-12-27 03:31:53 +13:00
-> inject ( 'events' )
2022-11-04 22:50:59 +13:00
-> inject ( 'project' )
2023-01-20 13:36:17 +13:00
-> action ( function ( ? string $sessionId , ? \DateTime $requestTimestamp , Request $request , Response $response , Document $user , Database $dbForProject , Locale $locale , Event $events , Document $project ) {
2023-08-24 08:23:52 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2020-06-30 09:43:34 +12:00
$sessionId = ( $sessionId === 'current' )
2022-11-14 22:30:45 +13:00
? Auth :: sessionVerify ( $user -> getAttribute ( 'sessions' ), Auth :: $secret , $authDuration )
2022-02-02 04:54:20 +13:00
: $sessionId ;
2021-08-05 17:06:38 +12:00
2021-02-20 01:12:47 +13:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2020-01-24 11:33:44 +13:00
2021-08-05 17:06:38 +12:00
foreach ( $sessions as $key => $session ) { /** @var Document $session */
2021-05-07 10:31:05 +12:00
if ( $sessionId == $session -> getId ()) {
2023-01-20 13:36:17 +13:00
$dbForProject -> withRequestTimestamp ( $requestTimestamp , function () use ( $dbForProject , $session ) {
return $dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
});
2020-01-24 11:33:44 +13:00
2023-01-20 13:36:17 +13:00
unset ( $sessions [ $key ]);
2021-07-17 22:04:43 +12:00
2021-02-20 01:12:47 +13:00
$session -> setAttribute ( 'current' , false );
2021-08-05 17:06:38 +12:00
2021-02-20 01:12:47 +13:00
if ( $session -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
2021-06-13 08:44:25 +12:00
$session
-> setAttribute ( 'current' , true )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'countryName' , $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' )))
;
2021-08-05 17:06:38 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2021-05-28 18:27:14 +12:00
$response
2023-08-24 08:23:52 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]))
;
2021-05-28 18:27:14 +12:00
}
2020-11-21 10:02:26 +13:00
2020-01-24 11:33:44 +13:00
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( Auth :: $cookieName , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
;
2020-01-24 11:33:44 +13:00
}
2022-05-24 02:54:50 +12:00
2022-04-26 22:36:49 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-05-07 10:31:05 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
2023-08-24 08:23:52 +12:00
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ))
;
2020-06-30 09:43:34 +12:00
return $response -> noContent ();
}
}
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2020-12-27 03:31:53 +13:00
});
2020-06-30 09:43:34 +12:00
2022-02-03 03:14:17 +13:00
App :: patch ( '/v1/account/sessions/:sessionId' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Update OAuth Session (Refresh Tokens)' )
2022-02-02 04:54:20 +13:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].update' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.update' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-12 23:01:12 +12:00
-> label ( 'audits.userId' , '{response.userId}' )
2022-02-02 04:54:20 +13:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
2022-02-03 03:14:17 +13:00
-> label ( 'sdk.method' , 'updateSession' )
-> label ( 'sdk.description' , '/docs/references/account/update-session.md' )
2022-02-02 04:54:20 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_SESSION )
-> label ( 'abuse-limit' , 10 )
2022-09-19 22:05:42 +12:00
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to update the current device session.' )
2022-02-02 04:54:20 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'project' )
-> inject ( 'locale' )
-> inject ( 'events' )
2022-08-10 20:45:10 +12:00
-> action ( function ( ? string $sessionId , Request $request , Response $response , Document $user , Database $dbForProject , Document $project , Locale $locale , Event $events ) {
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-02-02 04:54:20 +13:00
$sessionId = ( $sessionId === 'current' )
2022-11-14 22:30:45 +13:00
? Auth :: sessionVerify ( $user -> getAttribute ( 'sessions' ), Auth :: $secret , $authDuration )
2022-02-02 04:54:20 +13:00
: $sessionId ;
$sessions = $user -> getAttribute ( 'sessions' , []);
foreach ( $sessions as $key => $session ) { /** @var Document $session */
if ( $sessionId == $session -> getId ()) {
2022-02-03 03:14:17 +13:00
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
2022-02-02 05:47:08 +13:00
2022-02-02 04:54:20 +13:00
$provider = $session -> getAttribute ( 'provider' );
$refreshToken = $session -> getAttribute ( 'providerRefreshToken' );
2023-08-24 08:23:52 +12:00
$appId = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'authProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
2022-02-02 04:54:20 +13:00
2023-08-24 08:23:52 +12:00
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst ( $provider );
2022-04-14 00:39:31 +12:00
2023-08-24 08:23:52 +12:00
if ( ! \class_exists ( $className )) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
2022-02-04 00:57:04 +13:00
}
2022-02-02 04:54:20 +13:00
$oauth2 = new $className ( $appId , $appSecret , '' , [], []);
$oauth2 -> refreshTokens ( $refreshToken );
$session
-> setAttribute ( 'providerAccessToken' , $oauth2 -> getAccessToken ( '' ))
-> setAttribute ( 'providerRefreshToken' , $oauth2 -> getRefreshToken ( '' ))
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'providerAccessTokenExpiry' , DateTime :: addSeconds ( new \DateTime (), ( int ) $oauth2 -> getAccessTokenExpiry ( '' )));
2022-02-02 04:54:20 +13:00
2022-02-02 05:30:49 +13:00
$dbForProject -> updateDocument ( 'sessions' , $sessionId , $session );
2022-04-26 22:36:49 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2022-02-02 04:54:20 +13:00
2022-11-14 22:42:18 +13:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2022-11-05 03:48:29 +13:00
2023-05-23 01:04:51 +12:00
$session -> setAttribute ( 'expire' , DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime ( $session -> getCreatedAt ()), $authDuration )));
2022-11-04 04:03:39 +13:00
2022-02-02 04:54:20 +13:00
$events
2022-04-04 18:30:07 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
2023-08-24 08:23:52 +12:00
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ))
;
2022-02-02 04:54:20 +13:00
2022-02-02 05:47:08 +13:00
return $response -> dynamic ( $session , Response :: MODEL_SESSION );
2022-02-02 04:54:20 +13:00
}
}
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2022-02-02 04:54:20 +13:00
});
2020-06-30 09:43:34 +12:00
App :: delete ( '/v1/account/sessions' )
2022-11-04 04:24:32 +13:00
-> desc ( 'Delete Sessions' )
2020-06-30 09:43:34 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].delete' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'session.delete' )
2022-08-13 01:21:32 +12:00
-> label ( 'audits.resource' , 'user/{user.$id}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-06-30 09:43:34 +12:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'deleteSessions' )
-> label ( 'sdk.description' , '/docs/references/account/delete-sessions.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_NOCONTENT )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2020-06-30 09:43:34 +12:00
-> label ( 'abuse-limit' , 100 )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2021-06-13 08:44:25 +12:00
-> inject ( 'locale' )
2020-12-27 03:31:53 +13:00
-> inject ( 'events' )
2022-08-13 15:14:28 +12:00
-> action ( function ( Request $request , Response $response , Document $user , Database $dbForProject , Locale $locale , Event $events ) {
2023-08-24 08:23:52 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2021-02-20 02:59:36 +13:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2020-06-30 09:43:34 +12:00
2021-08-05 17:06:38 +12:00
foreach ( $sessions as $session ) { /** @var Document $session */
2021-12-28 01:45:23 +13:00
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2022-05-13 07:31:15 +12:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]));
2020-06-30 09:43:34 +12:00
}
2021-06-13 08:44:25 +12:00
$session
-> setAttribute ( 'current' , false )
2023-08-24 08:23:52 +12:00
-> setAttribute ( 'countryName' , $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' )))
;
2020-11-21 10:02:26 +13:00
2022-05-13 07:31:15 +12:00
if ( $session -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) {
2021-02-20 02:59:36 +13:00
$session -> setAttribute ( 'current' , true );
2022-11-05 03:48:29 +13:00
$session -> setAttribute ( 'expire' , DateTime :: addSeconds ( new \DateTime ( $session -> getCreatedAt ()), Auth :: TOKEN_EXPIRATION_LOGIN_LONG ));
2022-05-13 07:31:15 +12:00
2023-08-24 08:23:52 +12:00
// If current session delete the cookies too
2020-06-30 09:43:34 +12:00
$response
2023-08-24 08:23:52 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
2022-05-13 07:31:15 +12:00
-> addCookie ( Auth :: $cookieName , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ));
// Use current session for events.
$events -> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ));
2020-06-30 09:43:34 +12:00
}
2020-01-24 11:33:44 +13:00
}
2021-05-07 10:31:05 +12:00
2022-04-26 22:36:49 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-08-05 17:06:38 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-14 00:39:31 +12:00
-> setParam ( 'userId' , $user -> getId ())
2022-05-13 07:31:15 +12:00
-> setParam ( 'sessionId' , $session -> getId ());
2020-06-30 09:43:34 +12:00
$response -> noContent ();
2020-12-27 03:31:53 +13:00
});
2020-01-24 11:33:44 +13:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account/recovery' )
2020-02-09 07:51:49 +13:00
-> desc ( 'Create Password Recovery' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-06 12:07:41 +13:00
-> label ( 'scope' , 'public' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].recovery.[tokenId].create' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'recovery.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-01-06 12:07:41 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'createRecovery' )
2020-01-06 12:07:41 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create-recovery.md' )
2020-11-13 11:50:53 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TOKEN )
2020-01-06 12:07:41 +13:00
-> label ( 'abuse-limit' , 10 )
2021-11-09 23:21:24 +13:00
-> label ( 'abuse-key' , [ 'url:{url},email:{param-email}' , 'ip:{ip}' ])
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2022-05-24 04:34:03 +12:00
-> param ( 'url' , '' , fn ( $clients ) => new Host ( $clients ), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , false , [ 'clients' ])
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2020-12-27 03:31:53 +13:00
-> inject ( 'project' )
-> inject ( 'locale' )
-> inject ( 'mails' )
-> inject ( 'events' )
2023-07-07 12:12:39 +12:00
-> action ( function ( string $email , string $url , Request $request , Response $response , Document $user , Database $dbForProject , Document $project , Locale $locale , Mail $mails , Event $events ) {
2023-08-24 08:23:52 +12:00
2022-05-24 02:54:50 +12:00
if ( empty ( App :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-14 03:15:19 +12:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP Disabled' );
2021-08-31 19:35:04 +12:00
}
2021-12-11 06:52:33 +13:00
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2020-06-30 09:43:34 +12:00
2021-06-04 01:03:51 +12:00
$email = \strtolower ( $email );
2022-05-13 04:25:36 +12:00
$profile = $dbForProject -> findOne ( 'users' , [
2022-08-12 11:53:52 +12:00
Query :: equal ( 'email' , [ $email ]),
2022-05-13 04:25:36 +12:00
]);
2020-06-30 09:43:34 +12:00
2023-08-24 08:23:52 +12:00
if ( ! $profile ) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2020-06-30 09:43:34 +12:00
}
2020-01-12 02:58:02 +13:00
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2021-07-14 23:02:12 +12:00
if ( false === $profile -> getAttribute ( 'status' )) { // Account is blocked
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_BLOCKED );
2020-12-28 00:57:42 +13:00
}
2022-07-14 02:02:49 +12:00
$expire = DateTime :: addSeconds ( new \DateTime (), Auth :: TOKEN_EXPIRATION_RECOVERY );
2021-06-18 00:44:06 +12:00
2020-06-30 09:43:34 +12:00
$secret = Auth :: tokenGenerator ();
$recovery = new Document ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2021-06-13 08:44:25 +12:00
'userId' => $profile -> getId (),
2022-06-07 21:37:09 +12:00
'userInternalId' => $profile -> getInternalId (),
2020-06-30 09:43:34 +12:00
'type' => Auth :: TOKEN_TYPE_RECOVERY ,
2020-11-13 00:54:16 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
2021-07-07 00:18:55 +12:00
'expire' => $expire ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
]);
2021-08-05 17:06:38 +12:00
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $profile -> getId ()) -> toString ());
2020-01-12 02:58:02 +13:00
2022-04-27 23:06:53 +12:00
$recovery = $dbForProject -> createDocument ( 'tokens' , $recovery
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $profile -> getId ())),
Permission :: update ( Role :: user ( $profile -> getId ())),
Permission :: delete ( Role :: user ( $profile -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2020-01-06 12:07:41 +13:00
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $profile -> getId ());
2020-06-30 09:43:34 +12:00
$url = Template :: parseURL ( $url );
2021-07-07 00:18:55 +12:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $profile -> getId (), 'secret' => $secret , 'expire' => $expire ]);
2020-06-30 09:43:34 +12:00
$url = Template :: unParseURL ( $url );
2022-12-15 22:22:05 +13:00
$projectName = $project -> isEmpty () ? 'Console' : $project -> getAttribute ( 'name' , '[APP-NAME]' );
$from = $project -> isEmpty () || $project -> getId () === 'console' ? '' : \sprintf ( $locale -> getText ( 'emails.sender' ), $projectName );
2023-08-24 08:23:52 +12:00
$body = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-base.tpl' );
$subject = $locale -> getText ( " emails.recovery.subject " );
2022-12-15 22:22:05 +13:00
2023-04-19 20:29:29 +12:00
$smtpEnabled = $project -> getAttribute ( 'smtp' , [])[ 'enabled' ] ? ? false ;
2023-08-24 08:23:52 +12:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.recovery-' . $locale -> default ] ? ? [];
if ( $smtpEnabled && ! empty ( $customTemplate )) {
2023-07-18 19:05:09 +12:00
$body = $customTemplate [ 'message' ] ? ? $body ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
$from = $customTemplate [ 'senderName' ] ? ? $from ;
2023-04-19 20:29:29 +12:00
}
2022-12-15 22:22:05 +13:00
$body
-> setParam ( '{{subject}}' , $subject )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.recovery.hello " ))
2022-12-15 22:22:05 +13:00
-> setParam ( '{{name}}' , $profile -> getAttribute ( 'name' ))
2023-08-24 08:23:52 +12:00
-> setParam ( '{{body}}' , $locale -> getText ( " emails.recovery.body " ))
2022-12-15 22:22:05 +13:00
-> setParam ( '{{redirect}}' , $url )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.recovery.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.recovery.thanks " ))
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.recovery.signature " ))
2022-12-15 22:22:05 +13:00
-> setParam ( '{{project}}' , $projectName )
-> setParam ( '{{direction}}' , $locale -> getText ( 'settings.direction' ))
-> setParam ( '{{bg-body}}' , '#f7f7f7' )
-> setParam ( '{{bg-content}}' , '#ffffff' )
-> setParam ( '{{text-content}}' , '#000000' );
$body = $body -> render ();
2023-08-24 08:23:52 +12:00
2020-07-06 02:19:59 +12:00
$mails
2022-04-14 00:39:31 +12:00
-> setRecipient ( $profile -> getAttribute ( 'email' , '' ))
-> setName ( $profile -> getAttribute ( 'name' ))
2022-12-15 22:22:05 +13:00
-> setBody ( $body )
-> setFrom ( $from )
-> setSubject ( $subject )
2020-06-30 09:43:34 +12:00
-> trigger ();
2023-08-24 08:23:52 +12:00
;
2020-06-30 09:43:34 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-14 00:39:31 +12:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'tokenId' , $recovery -> getId ())
2022-04-19 04:21:45 +12:00
-> setUser ( $profile )
2022-04-14 00:39:31 +12:00
-> setPayload ( $response -> output (
$recovery -> setAttribute ( 'secret' , $secret ),
Response :: MODEL_TOKEN
2023-08-24 08:23:52 +12:00
))
;
2020-11-19 08:38:31 +13:00
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$recovery -> setAttribute ( 'secret' , ( $isPrivilegedUser || $isAppUser ) ? $secret : '' );
2020-11-19 08:38:31 +13:00
2022-09-07 23:11:10 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $recovery , Response :: MODEL_TOKEN );
2020-12-27 03:31:53 +13:00
});
2020-01-06 12:07:41 +13:00
2020-06-29 05:31:21 +12:00
App :: put ( '/v1/account/recovery' )
2021-08-31 19:13:30 +12:00
-> desc ( 'Create Password Recovery (confirmation)' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-06 12:07:41 +13:00
-> label ( 'scope' , 'public' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].recovery.[tokenId].update' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'recovery.update' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-12 23:01:12 +12:00
-> label ( 'audits.userId' , '{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-01-06 12:07:41 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'updateRecovery' )
2020-01-06 12:07:41 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-recovery.md' )
2020-11-13 11:50:53 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TOKEN )
2020-01-06 12:07:41 +13:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2021-12-11 01:27:11 +13:00
-> param ( 'userId' , '' , new UID (), 'User ID.' )
2020-09-11 02:40:14 +12:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid reset token.' )
2021-12-11 01:27:11 +13:00
-> param ( 'password' , '' , new Password (), 'New user password. Must be at least 8 chars.' )
-> param ( 'passwordAgain' , '' , new Password (), 'Repeat new user password. Must be at least 8 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
2023-07-07 12:12:39 +12:00
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2023-07-20 09:34:27 +12:00
-> inject ( 'project' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2023-07-21 01:41:28 +12:00
-> action ( function ( string $userId , string $secret , string $password , string $passwordAgain , Response $response , Document $user , Database $dbForProject , Document $project , Event $events ) {
2020-06-30 09:43:34 +12:00
if ( $password !== $passwordAgain ) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_PASSWORD_MISMATCH );
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
2021-12-28 01:45:23 +13:00
$profile = $dbForProject -> getDocument ( 'users' , $userId );
2020-01-06 12:07:41 +13:00
2022-05-16 21:58:17 +12:00
if ( $profile -> isEmpty ()) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
2021-05-07 10:31:05 +12:00
$tokens = $profile -> getAttribute ( 'tokens' , []);
$recovery = Auth :: tokenVerify ( $tokens , Auth :: TOKEN_TYPE_RECOVERY , $secret );
2020-01-06 12:07:41 +13:00
2023-08-24 08:23:52 +12:00
if ( ! $recovery ) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2020-06-30 09:43:34 +12:00
}
2020-01-06 12:07:41 +13:00
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $profile -> getId ()) -> toString ());
2020-01-06 12:07:41 +13:00
2023-07-20 09:34:27 +12:00
$newPassword = Auth :: passwordHash ( $password , Auth :: DEFAULT_ALGO , Auth :: DEFAULT_ALGO_OPTIONS );
$historyLimit = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
$history = $profile -> getAttribute ( 'passwordHistory' , []);
if ( $historyLimit > 0 ) {
$validator = new PasswordHistory ( $history , $profile -> getAttribute ( 'hash' ), $profile -> getAttribute ( 'hashOptions' ));
2023-08-24 08:23:52 +12:00
if ( ! $validator -> isValid ( $password )) {
2023-07-20 09:34:27 +12:00
throw new Exception ( Exception :: USER_PASSWORD_RECENTLY_USED );
}
$history [] = $newPassword ;
$history = array_slice ( $history , ( count ( $history ) - $historyLimit ), $historyLimit );
}
2021-12-28 01:45:23 +13:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile
2023-07-20 09:34:27 +12:00
-> setAttribute ( 'password' , $newPassword )
-> setAttribute ( 'passwordHistory' , $history )
2023-02-20 14:51:56 +13:00
-> setAttribute ( 'passwordUpdate' , DateTime :: now ())
2022-06-14 00:53:28 +12:00
-> setAttribute ( 'hash' , Auth :: DEFAULT_ALGO )
-> setAttribute ( 'hashOptions' , Auth :: DEFAULT_ALGO_OPTIONS )
2022-05-24 02:54:50 +12:00
-> setAttribute ( 'emailVerification' , true ));
2020-01-06 12:07:41 +13:00
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2022-04-27 23:06:53 +12:00
$recoveryDocument = $dbForProject -> getDocument ( 'tokens' , $recovery );
2020-06-30 09:43:34 +12:00
/**
2021-08-15 06:56:28 +12:00
* We act like we ' re updating and validating
* the recovery token but actually we don ' t need it anymore .
*/
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteDocument ( 'tokens' , $recovery );
$dbForProject -> deleteCachedDocument ( 'users' , $profile -> getId ());
2021-05-07 10:31:05 +12:00
2022-04-04 18:30:07 +12:00
$events
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $profile -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'tokenId' , $recoveryDocument -> getId ())
;
2020-01-06 12:07:41 +13:00
2022-04-27 23:06:53 +12:00
$response -> dynamic ( $recoveryDocument , Response :: MODEL_TOKEN );
2020-12-27 03:31:53 +13:00
});
2020-01-12 13:20:35 +13:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account/verification' )
2020-02-10 10:37:28 +13:00
-> desc ( 'Create Email Verification' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 13:20:35 +13:00
-> label ( 'scope' , 'account' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].verification.[tokenId].create' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'verification.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'createVerification' )
2022-06-21 02:47:49 +12:00
-> label ( 'sdk.description' , '/docs/references/account/create-email-verification.md' )
2020-11-13 11:50:53 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TOKEN )
2020-01-12 13:20:35 +13:00
-> label ( 'abuse-limit' , 10 )
2021-07-27 23:21:26 +12:00
-> label ( 'abuse-key' , 'url:{url},userId:{userId}' )
2023-08-24 08:23:52 +12:00
-> param ( 'url' , '' , fn ( $clients ) => new Host ( $clients ), 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , false , [ 'clients' ]) // TODO add built-in confirm page
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2020-12-27 03:31:53 +13:00
-> inject ( 'locale' )
-> inject ( 'events' )
-> inject ( 'mails' )
2022-08-10 20:45:10 +12:00
-> action ( function ( string $url , Request $request , Response $response , Document $project , Document $user , Database $dbForProject , Locale $locale , Event $events , Mail $mails ) {
2023-08-24 08:23:52 +12:00
2022-05-24 02:54:50 +12:00
if ( empty ( App :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-14 03:15:19 +12:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP Disabled' );
2021-08-31 19:35:04 +12:00
}
2021-12-11 06:52:33 +13:00
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2020-06-30 09:43:34 +12:00
$verificationSecret = Auth :: tokenGenerator ();
2022-07-14 02:02:49 +12:00
$expire = DateTime :: addSeconds ( new \DateTime (), Auth :: TOKEN_EXPIRATION_CONFIRM );
2021-08-05 17:06:38 +12:00
2020-06-30 09:43:34 +12:00
$verification = new Document ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2021-06-13 08:44:25 +12:00
'userId' => $user -> getId (),
2022-06-07 21:37:09 +12:00
'userInternalId' => $user -> getInternalId (),
2020-06-30 09:43:34 +12:00
'type' => Auth :: TOKEN_TYPE_VERIFICATION ,
2020-11-13 00:54:16 +13:00
'secret' => Auth :: hash ( $verificationSecret ), // One way hash encryption to protect DB leak
2021-07-07 00:18:55 +12:00
'expire' => $expire ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
]);
2021-08-05 17:06:38 +12:00
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-01-12 13:20:35 +13:00
2022-04-27 23:06:53 +12:00
$verification = $dbForProject -> createDocument ( 'tokens' , $verification
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2020-01-12 13:20:35 +13:00
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2021-06-18 00:44:06 +12:00
2020-06-30 09:43:34 +12:00
$url = Template :: parseURL ( $url );
2021-07-07 00:18:55 +12:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getId (), 'secret' => $verificationSecret , 'expire' => $expire ]);
2020-06-30 09:43:34 +12:00
$url = Template :: unParseURL ( $url );
2022-12-15 22:22:05 +13:00
$projectName = $project -> isEmpty () ? 'Console' : $project -> getAttribute ( 'name' , '[APP-NAME]' );
2022-12-14 19:35:04 +13:00
$from = $project -> isEmpty () || $project -> getId () === 'console' ? '' : \sprintf ( $locale -> getText ( 'emails.sender' ), $projectName );
2023-08-24 08:23:52 +12:00
$body = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-base.tpl' );
$subject = $locale -> getText ( " emails.verification.subject " );
2023-04-19 20:29:29 +12:00
$smtpEnabled = $project -> getAttribute ( 'smtp' , [])[ 'enabled' ] ? ? false ;
2023-08-24 08:23:52 +12:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.verification-' . $locale -> default ] ? ? [];
if ( $smtpEnabled && ! empty ( $customTemplate )) {
2023-07-18 19:05:09 +12:00
$body = $customTemplate [ 'message' ] ? ? $body ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
$from = $customTemplate [ 'senderName' ] ? ? $from ;
2023-04-19 20:29:29 +12:00
}
2022-12-14 19:35:04 +13:00
$body
-> setParam ( '{{subject}}' , $subject )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.verification.hello " ))
2022-12-15 22:22:05 +13:00
-> setParam ( '{{name}}' , $user -> getAttribute ( 'name' ))
2023-08-24 08:23:52 +12:00
-> setParam ( '{{body}}' , $locale -> getText ( " emails.verification.body " ))
2022-12-14 19:35:04 +13:00
-> setParam ( '{{redirect}}' , $url )
2023-08-24 08:23:52 +12:00
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.verification.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.verification.thanks " ))
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.verification.signature " ))
2022-12-14 19:35:04 +13:00
-> setParam ( '{{project}}' , $projectName )
-> setParam ( '{{direction}}' , $locale -> getText ( 'settings.direction' ))
-> setParam ( '{{bg-body}}' , '#f7f7f7' )
-> setParam ( '{{bg-content}}' , '#ffffff' )
-> setParam ( '{{text-content}}' , '#000000' );
$body = $body -> render ();
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$mails
2022-12-14 19:35:04 +13:00
-> setSubject ( $subject )
-> setBody ( $body )
-> setFrom ( $from )
2022-04-14 00:39:31 +12:00
-> setRecipient ( $user -> getAttribute ( 'email' ))
2023-06-24 20:27:02 +12:00
-> setName ( $user -> getAttribute ( 'name' ) ? ? '' )
2023-08-24 08:23:52 +12:00
-> trigger ()
;
2020-06-30 09:43:34 +12:00
2020-12-07 11:14:57 +13:00
$events
2022-04-04 18:30:07 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $verification -> getId ())
2022-04-14 00:39:31 +12:00
-> setPayload ( $response -> output (
$verification -> setAttribute ( 'secret' , $verificationSecret ),
Response :: MODEL_TOKEN
2023-08-24 08:23:52 +12:00
))
;
2020-11-19 08:38:31 +13:00
2022-04-19 04:21:45 +12:00
// Hide secret for clients
$verification -> setAttribute ( 'secret' , ( $isPrivilegedUser || $isAppUser ) ? $verificationSecret : '' );
2020-11-19 08:38:31 +13:00
2022-09-07 23:11:10 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $verification , Response :: MODEL_TOKEN );
2020-12-27 03:31:53 +13:00
});
2020-01-12 13:20:35 +13:00
2020-06-29 05:31:21 +12:00
App :: put ( '/v1/account/verification' )
2021-08-31 19:13:30 +12:00
-> desc ( 'Create Email Verification (confirmation)' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 13:20:35 +13:00
-> label ( 'scope' , 'public' )
2022-04-04 18:30:07 +12:00
-> label ( 'event' , 'users.[userId].verification.[tokenId].update' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'verification.update' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2021-04-16 19:22:17 +12:00
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'updateVerification' )
2022-06-21 02:47:49 +12:00
-> label ( 'sdk.description' , '/docs/references/account/update-email-verification.md' )
2020-11-13 11:50:53 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TOKEN )
2020-01-12 13:20:35 +13:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2021-12-11 01:27:11 +13:00
-> param ( 'userId' , '' , new UID (), 'User ID.' )
2020-09-11 02:40:14 +12:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-28 01:45:23 +13:00
-> inject ( 'dbForProject' )
2022-04-04 18:30:07 +12:00
-> inject ( 'events' )
2022-08-10 20:45:10 +12:00
-> action ( function ( string $userId , string $secret , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
$profile = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2020-06-30 09:43:34 +12:00
2021-05-07 10:31:05 +12:00
if ( $profile -> isEmpty ()) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2020-06-30 09:43:34 +12:00
}
2020-01-12 13:20:35 +13:00
2021-05-07 10:31:05 +12:00
$tokens = $profile -> getAttribute ( 'tokens' , []);
$verification = Auth :: tokenVerify ( $tokens , Auth :: TOKEN_TYPE_VERIFICATION , $secret );
2020-01-12 13:20:35 +13:00
2023-08-24 08:23:52 +12:00
if ( ! $verification ) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2020-06-30 09:43:34 +12:00
}
2020-01-12 13:20:35 +13:00
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $profile -> getId ()) -> toString ());
2020-01-12 13:20:35 +13:00
2021-12-28 01:45:23 +13:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile -> setAttribute ( 'emailVerification' , true ));
2020-01-12 13:20:35 +13:00
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2022-04-27 23:06:53 +12:00
$verificationDocument = $dbForProject -> getDocument ( 'tokens' , $verification );
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
/**
2021-08-15 06:56:28 +12:00
* We act like we ' re updating and validating
* the verification token but actually we don ' t need it anymore .
*/
2022-04-27 23:06:53 +12:00
$dbForProject -> deleteDocument ( 'tokens' , $verification );
$dbForProject -> deleteCachedDocument ( 'users' , $profile -> getId ());
2021-05-07 10:31:05 +12:00
2022-04-04 18:30:07 +12:00
$events
2023-03-11 07:53:49 +13:00
-> setParam ( 'userId' , $userId )
2023-08-24 08:23:52 +12:00
-> setParam ( 'tokenId' , $verificationDocument -> getId ())
;
2022-04-04 18:30:07 +12:00
2022-04-27 23:06:53 +12:00
$response -> dynamic ( $verificationDocument , Response :: MODEL_TOKEN );
2021-06-13 02:50:40 +12:00
});
2022-06-09 00:50:31 +12:00
App :: post ( '/v1/account/verification/phone' )
-> desc ( 'Create Phone Verification' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'event' , 'users.[userId].verification.[tokenId].create' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'verification.create' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-06-09 00:50:31 +12:00
-> 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 ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2022-06-09 01:57:34 +12:00
-> inject ( 'messaging' )
2023-04-19 20:29:29 +12:00
-> inject ( 'project' )
-> inject ( 'locale' )
-> action ( function ( Request $request , Response $response , Document $user , Database $dbForProject , Event $events , EventPhone $messaging , Document $project , Locale $locale ) {
2023-08-24 08:23:52 +12:00
2022-08-15 06:09:24 +12:00
if ( empty ( App :: getEnv ( '_APP_SMS_PROVIDER' ))) {
2022-08-15 20:30:58 +12:00
throw new Exception ( Exception :: GENERAL_PHONE_DISABLED );
2022-06-09 00:50:31 +12:00
}
if ( empty ( $user -> getAttribute ( 'phone' ))) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_PHONE_NOT_FOUND );
2022-06-09 00:50:31 +12:00
}
$roles = Authorization :: getRoles ();
$isPrivilegedUser = Auth :: isPrivilegedUser ( $roles );
$isAppUser = Auth :: isAppUser ( $roles );
2022-09-19 20:09:48 +12:00
$secret = Auth :: codeGenerator ();
2022-07-14 02:02:49 +12:00
$expire = DateTime :: addSeconds ( new \DateTime (), Auth :: TOKEN_EXPIRATION_CONFIRM );
2022-06-09 00:50:31 +12:00
$verification = new Document ([
2022-08-15 02:22:38 +12:00
'$id' => ID :: unique (),
2022-06-09 00:50:31 +12:00
'userId' => $user -> getId (),
2022-06-21 09:38:45 +12:00
'userInternalId' => $user -> getInternalId (),
2022-06-09 00:50:31 +12:00
'type' => Auth :: TOKEN_TYPE_PHONE ,
2022-09-23 10:25:17 +12:00
'secret' => Auth :: hash ( $secret ),
2022-06-09 00:50:31 +12:00
'expire' => $expire ,
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $user -> getId ()) -> toString ());
2022-06-09 00:50:31 +12:00
$verification = $dbForProject -> createDocument ( 'tokens' , $verification
2022-08-02 21:21:53 +12:00
-> setAttribute ( '$permissions' , [
2022-08-15 23:24:31 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 21:21:53 +12:00
]));
2022-06-09 00:50:31 +12:00
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2023-08-24 08:23:52 +12:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/sms-base.tpl' );
2023-04-19 20:29:29 +12:00
2023-08-24 08:23:52 +12:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'sms.verification-' . $locale -> default ] ? ? [];
if ( ! empty ( $customTemplate )) {
2023-07-18 19:05:09 +12:00
$message = $customTemplate [ 'message' ] ? ? $message ;
2023-04-19 20:29:29 +12:00
}
2023-03-17 13:58:21 +13:00
$message = $message -> setParam ( '{{token}}' , $secret );
2023-03-14 22:07:42 +13:00
$message = $message -> render ();
2022-06-09 00:50:31 +12:00
2022-06-09 01:57:34 +12:00
$messaging
-> setRecipient ( $user -> getAttribute ( 'phone' ))
2023-03-14 22:07:42 +13:00
-> setMessage ( $message )
2023-08-24 08:23:52 +12:00
-> trigger ()
;
2022-06-09 00:50:31 +12:00
$events
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $verification -> getId ())
-> setPayload ( $response -> output (
2023-06-23 12:54:09 +12:00
$verification -> setAttribute ( 'secret' , $secret ),
2022-06-09 00:50:31 +12:00
Response :: MODEL_TOKEN
2023-08-24 08:23:52 +12:00
))
;
2022-06-09 00:50:31 +12:00
// Hide secret for clients
2023-06-23 12:54:09 +12:00
$verification -> setAttribute ( 'secret' , ( $isPrivilegedUser || $isAppUser ) ? $secret : '' );
2022-06-09 00:50:31 +12:00
2022-09-07 23:11:10 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $verification , Response :: MODEL_TOKEN );
2022-06-09 00:50:31 +12:00
});
App :: put ( '/v1/account/verification/phone' )
-> desc ( 'Create Phone Verification (confirmation)' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'public' )
-> label ( 'event' , 'users.[userId].verification.[tokenId].update' )
2022-09-05 20:00:08 +12:00
-> label ( 'audits.event' , 'verification.update' )
2022-08-12 01:19:05 +12:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-06-09 00:50:31 +12:00
-> 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 ( 'events' )
2022-08-10 20:45:10 +12:00
-> action ( function ( string $userId , string $secret , Response $response , Document $user , Database $dbForProject , Event $events ) {
2023-08-24 08:23:52 +12:00
$profile = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2022-06-09 00:50:31 +12:00
if ( $profile -> isEmpty ()) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2022-06-09 00:50:31 +12:00
}
$verification = Auth :: phoneTokenVerify ( $user -> getAttribute ( 'tokens' , []), $secret );
2023-08-24 08:23:52 +12:00
if ( ! $verification ) {
2022-07-27 02:24:32 +12:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2022-06-09 00:50:31 +12:00
}
2022-08-19 16:04:33 +12:00
Authorization :: setRole ( Role :: user ( $profile -> getId ()) -> toString ());
2022-06-09 00:50:31 +12:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile -> setAttribute ( 'phoneVerification' , true ));
2023-07-07 12:12:39 +12:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2022-06-09 00:50:31 +12:00
$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 ());
$events
-> setParam ( 'userId' , $user -> getId ())
2023-08-24 08:23:52 +12:00
-> setParam ( 'tokenId' , $verificationDocument -> getId ())
;
2022-06-09 00:50:31 +12:00
$response -> dynamic ( $verificationDocument , Response :: MODEL_TOKEN );
});
2023-08-19 05:58:12 +12:00
App :: post ( '/v1/account/targets' )
2023-08-24 08:24:25 +12:00
-> desc ( 'Create Account\'s Target' )
2023-08-19 05:58:12 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].targets.[targetId].create' )
2023-08-24 08:24:25 +12:00
-> label ( 'audits.event' , 'target.create' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2023-08-19 05:58:12 +12:00
-> label ( 'scope' , 'targets.write' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'createTarget' )
-> label ( 'sdk.description' , '/docs/references/account/create-target.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_CREATED )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TARGET )
2023-08-24 08:24:25 +12:00
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
-> param ( 'providerId' , '' , new UID (), 'Provider ID.' )
-> param ( 'identifier' , '' , new Text ( Database :: LENGTH_KEY ), 'The target identifier (token, email, phone etc.)' )
-> inject ( 'user' )
2023-08-19 05:58:12 +12:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2023-08-24 08:24:25 +12:00
-> action ( function ( string $targetId , string $providerId , string $identifier , Document $user , Response $response , Database $dbForProject , Event $events ) {
$provider = Authorization :: skip ( fn () => $dbForProject -> getDocument ( 'providers' , $providerId ));
2023-08-19 05:58:12 +12:00
if ( $provider -> isEmpty ()) {
throw new Exception ( Exception :: PROVIDER_NOT_FOUND );
}
$target = $dbForProject -> getDocument ( 'targets' , $targetId );
2023-08-24 08:23:52 +12:00
if ( ! $target -> isEmpty ()) {
2023-08-19 05:58:12 +12:00
throw new Exception ( Exception :: USER_TARGET_ALREADY_EXISTS );
}
$target = $dbForProject -> createDocument ( 'targets' , new Document ([
'$id' => $targetId ,
'$permissions' => [
2023-08-24 08:24:25 +12:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2023-08-19 05:58:12 +12:00
],
'providerId' => $providerId ,
'providerInternalId' => $provider -> getInternalId (),
'providerType' => null ,
2023-08-24 08:24:25 +12:00
'userId' => $user -> getId (),
2023-08-19 05:58:12 +12:00
'userInternalId' => $user -> getInternalId (),
'identifier' => $identifier ,
]));
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
$events
2023-08-24 08:24:25 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'targetId' , $targetId );
2023-08-19 05:58:12 +12:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $target , Response :: MODEL_TARGET );
});
App :: patch ( '/v1/account/targets/:targetId/identifier' )
2023-08-24 08:24:25 +12:00
-> desc ( 'Update account\'s target identifier' )
2023-08-19 05:58:12 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].targets.[targetId].update' )
2023-08-24 08:24:25 +12:00
-> label ( 'audits.event' , 'target.update' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2023-08-19 05:58:12 +12:00
-> label ( 'scope' , 'targets.write' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'updateTargetIdentifier' )
-> label ( 'sdk.description' , '/docs/references/account/update-target-identifier.md' )
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_OK )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_TARGET )
2023-08-24 08:24:25 +12:00
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
-> param ( 'identifier' , '' , new Text ( Database :: LENGTH_KEY ), 'The target identifier (token, email, phone etc.)' )
-> inject ( 'user' )
2023-08-19 05:58:12 +12:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2023-08-24 08:24:25 +12:00
-> action ( function ( string $targetId , string $identifier , Document $user , Response $response , Database $dbForProject , Event $events ) {
2023-08-19 05:58:12 +12:00
$target = $dbForProject -> getDocument ( 'targets' , $targetId );
if ( $target -> isEmpty ()) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
}
// Update the target identifier here
$target -> setAttribute ( 'identifier' , $identifier );
$target = $dbForProject -> updateDocument ( 'targets' , $target -> getId (), $target );
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
$events
2023-08-24 08:24:25 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'targetId' , $targetId );
2023-08-19 05:58:12 +12:00
$response
-> dynamic ( $target , Response :: MODEL_TARGET );
});
App :: delete ( '/v1/account/targets/:targetId' )
2023-08-24 08:24:25 +12:00
-> desc ( 'Delete account\'s target' )
2023-08-19 05:58:12 +12:00
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].targets.[targetId].delete' )
2023-08-24 08:24:25 +12:00
-> label ( 'audits.event' , 'target.delete' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2023-08-19 05:58:12 +12:00
-> label ( 'scope' , 'targets.write' )
-> label ( 'sdk.auth' , [ APP_AUTH_TYPE_SESSION , APP_AUTH_TYPE_JWT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'deleteTarget' )
-> label ( 'sdk.description' , '/docs/references/account/delete-target.md' )
2023-08-24 08:24:25 +12:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_NOCONTENT )
2023-08-19 05:58:12 +12:00
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2023-08-24 08:24:25 +12:00
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
-> inject ( 'user' )
2023-08-19 05:58:12 +12:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'events' )
2023-08-24 08:24:25 +12:00
-> action ( function ( string $targetId , Document $user , Response $response , Database $dbForProject , Event $events ) {
2023-08-19 05:58:12 +12:00
$target = $dbForProject -> getDocument ( 'targets' , $targetId );
if ( $target -> isEmpty ()) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
}
$target = $dbForProject -> deleteDocument ( 'targets' , $target -> getId ());
$dbForProject -> deleteCachedDocument ( 'users' , $user -> getId ());
2023-08-24 08:24:25 +12:00
$user = $dbForProject -> getDocument ( 'users' , $user -> getId ());
2023-08-19 05:58:12 +12:00
// clone user object to send to workers
$clone = clone $user ;
$events
2023-08-24 08:24:25 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'targetId' , $targetId )
2023-08-19 05:58:12 +12:00
-> setPayload ( $response -> output ( $clone , Response :: MODEL_USER ));
$response -> noContent ();
});