2019-05-09 18:54:39 +12:00
< ? php
2020-10-18 06:49:09 +13:00
use Ahc\Jwt\JWT ;
2020-06-29 05:31:21 +12:00
use Utopia\App ;
2019-05-09 18:54:39 +12:00
use Utopia\Exception ;
2020-03-29 01:42:16 +13:00
use Utopia\Config\Config ;
2020-01-19 08:07:02 +13:00
use Utopia\Validator\Assoc ;
2019-05-09 18:54:39 +12:00
use Utopia\Validator\Text ;
use Utopia\Validator\Email ;
2020-01-04 10:00:53 +13:00
use Utopia\Validator\WhiteList ;
use Utopia\Validator\Host ;
use Utopia\Validator\URL ;
2019-12-29 22:47:55 +13:00
use Utopia\Audit\Audit ;
use Utopia\Audit\Adapters\MySQL as AuditAdapter ;
2020-03-25 06:56:32 +13:00
use Appwrite\Auth\Auth ;
use Appwrite\Auth\Validator\Password ;
use Appwrite\Database\Database ;
use Appwrite\Database\Document ;
use Appwrite\Database\Exception\Duplicate ;
use Appwrite\Database\Validator\UID ;
use Appwrite\Database\Validator\Authorization ;
use Appwrite\Template\Template ;
use Appwrite\OpenSSL\OpenSSL ;
2020-04-11 06:59:14 +12:00
use Appwrite\URL\URL as URLParser ;
2020-06-24 03:01:20 +12:00
use Appwrite\Utopia\Response ;
2019-05-09 18:54:39 +12:00
use DeviceDetector\DeviceDetector ;
2020-06-17 07:37:46 +12:00
use Utopia\Validator\ArrayList ;
2019-05-09 18:54:39 +12:00
2020-07-15 04:29:49 +12:00
$oauthDefaultSuccess = App :: getEnv ( '_APP_HOME' ) . '/auth/oauth2/success' ;
$oauthDefaultFailure = App :: getEnv ( '_APP_HOME' ) . '/auth/oauth2/failure' ;
2020-04-09 01:38:36 +12:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account' )
2020-01-23 12:32:10 +13:00
-> desc ( 'Create Account' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-01-04 10:00:53 +13:00
-> label ( 'abuse-limit' , 10 )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
-> param ( 'password' , '' , new Password (), 'User password. Must be between 6 to 32 chars.' )
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $email , $password , $name , $request , $response , $project , $projectDB , $audits ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
if ( 'console' === $project -> getId ()) {
$whitlistEmails = $project -> getAttribute ( 'authWhitelistEmails' );
$whitlistIPs = $project -> getAttribute ( 'authWhitelistIPs' );
$whitlistDomains = $project -> getAttribute ( 'authWhitelistDomains' );
if ( ! empty ( $whitlistEmails ) && ! \in_array ( $email , $whitlistEmails )) {
throw new Exception ( 'Console registration is restricted to specific emails. Contact your administrator for more information.' , 401 );
2020-01-04 10:00:53 +13:00
}
2020-06-30 09:43:34 +12:00
if ( ! empty ( $whitlistIPs ) && ! \in_array ( $request -> getIP (), $whitlistIPs )) {
throw new Exception ( 'Console registration is restricted to specific IPs. Contact your administrator for more information.' , 401 );
2020-01-04 10:00:53 +13:00
}
2020-06-30 09:43:34 +12:00
if ( ! empty ( $whitlistDomains ) && ! \in_array ( \substr ( \strrchr ( $email , '@' ), 1 ), $whitlistDomains )) {
throw new Exception ( 'Console registration is restricted to specific domains. Contact your administrator for more information.' , 401 );
2020-03-22 10:10:06 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
if ( ! empty ( $profile )) {
throw new Exception ( 'Account already exists' , 409 );
}
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: disable ();
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
try {
$user = $projectDB -> createDocument ([
'$collection' => Database :: SYSTEM_COLLECTION_USERS ,
'$permissions' => [
'read' => [ '*' ],
'write' => [ 'user:{self}' ],
],
'email' => $email ,
'emailVerification' => false ,
'status' => Auth :: USER_STATUS_UNACTIVATED ,
'password' => Auth :: passwordHash ( $password ),
2021-01-15 01:04:16 +13:00
'passwordUpdate' => \time (),
2020-06-30 09:43:34 +12:00
'registration' => \time (),
'reset' => false ,
'name' => $name ,
], [ 'email' => $email ]);
} catch ( Duplicate $th ) {
throw new Exception ( 'Account already exists' , 409 );
}
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: enable ();
2020-01-04 10:00:53 +13:00
2020-11-21 10:02:26 +13:00
Authorization :: unsetRole ( 'role:' . Auth :: USER_ROLE_GUEST );
Authorization :: setRole ( 'user:' . $user -> getId ());
Authorization :: setRole ( 'role:' . Auth :: USER_ROLE_MEMBER );
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
2020-01-05 04:45:28 +13:00
}
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.create' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-10-31 21:42:41 +13:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $user , Response :: MODEL_USER )
;
2020-12-27 03:31:53 +13:00
});
2020-01-05 04:45:28 +13:00
2020-06-29 05:31:21 +12:00
App :: post ( '/v1/account/sessions' )
2020-01-06 00:29:42 +13:00
-> desc ( 'Create Account Session' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.sessions.create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
2020-01-05 04:45:28 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'createSession' )
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create-session.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.' )
-> param ( 'password' , '' , new Password (), 'User password. Must be between 6 to 32 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'projectDB' )
-> inject ( 'locale' )
-> inject ( 'geodb' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $email , $password , $request , $response , $projectDB , $locale , $geodb , $audits ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-07-03 09:48:02 +12:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Database $projectDB */
2020-10-31 08:53:27 +13:00
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
if ( false == $profile || ! Auth :: passwordVerify ( $password , $profile -> getAttribute ( 'password' ))) {
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
//->setParam('userId', $profile->getId())
-> setParam ( 'event' , 'account.sesssions.failed' )
-> setParam ( 'resource' , 'users/' . ( $profile ? $profile -> getId () : '' ))
;
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
throw new Exception ( 'Invalid credentials' , 401 ); // Wrong password or username
}
2020-01-04 10:00:53 +13:00
2020-12-28 00:57:42 +13:00
if ( Auth :: USER_STATUS_BLOCKED == $profile -> getAttribute ( 'status' )) { // Account is blocked
throw new Exception ( 'Invalid credentials. User is blocked' , 401 ); // User is in status blocked
}
2020-10-31 08:53:27 +13:00
$dd = new DeviceDetector ( $request -> getUserAgent ( 'UNKNOWN' ));
$dd -> parse ();
$os = $dd -> getOs ();
$osCode = ( isset ( $os [ 'short_name' ])) ? $os [ 'short_name' ] : '' ;
$osName = ( isset ( $os [ 'name' ])) ? $os [ 'name' ] : '' ;
$osVersion = ( isset ( $os [ 'version' ])) ? $os [ 'version' ] : '' ;
$client = $dd -> getClient ();
$clientType = ( isset ( $client [ 'type' ])) ? $client [ 'type' ] : '' ;
$clientCode = ( isset ( $client [ 'short_name' ])) ? $client [ 'short_name' ] : '' ;
$clientName = ( isset ( $client [ 'name' ])) ? $client [ 'name' ] : '' ;
$clientVersion = ( isset ( $client [ 'version' ])) ? $client [ 'version' ] : '' ;
$clientEngine = ( isset ( $client [ 'engine' ])) ? $client [ 'engine' ] : '' ;
$clientEngineVersion = ( isset ( $client [ 'engine_version' ])) ? $client [ 'engine_version' ] : '' ;
2020-06-30 09:43:34 +12:00
$expiry = \time () + Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
$secret = Auth :: tokenGenerator ();
$session = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $profile -> getId ()], 'write' => [ 'user:' . $profile -> getId ()]],
2020-11-26 19:12:24 +13:00
'userId' => $profile -> getId (),
2020-06-30 09:43:34 +12:00
'type' => Auth :: TOKEN_TYPE_LOGIN ,
2020-11-13 00:54:16 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
2020-06-30 09:43:34 +12:00
'expire' => $expiry ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
2020-10-31 08:53:27 +13:00
'osCode' => $osCode ,
'osName' => $osName ,
'osVersion' => $osVersion ,
'clientType' => $clientType ,
'clientCode' => $clientCode ,
'clientName' => $clientName ,
'clientVersion' => $clientVersion ,
'clientEngine' => $clientEngine ,
'clientEngineVersion' => $clientEngineVersion ,
'deviceName' => $dd -> getDeviceName (),
'deviceBrand' => $dd -> getBrandName (),
'deviceModel' => $dd -> getModel (),
2020-06-30 09:43:34 +12:00
]);
2020-01-05 04:45:28 +13:00
2020-10-31 08:53:27 +13:00
$record = $geodb -> get ( $request -> getIP ());
if ( $record ) {
$session
-> setAttribute ( 'countryCode' , \strtolower ( $record [ 'country' ][ 'iso_code' ]))
;
} else {
$session
-> setAttribute ( 'countryCode' , '--' )
;
}
2020-06-30 09:43:34 +12:00
Authorization :: setRole ( 'user:' . $profile -> getId ());
2020-01-04 10:00:53 +13:00
2020-06-30 09:43:34 +12:00
$session = $projectDB -> createDocument ( $session -> getArrayCopy ());
2020-01-12 02:58:02 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $session ) {
throw new Exception ( 'Failed saving session to DB' , 500 );
}
2020-01-12 02:58:02 +13:00
2020-06-30 09:43:34 +12:00
$profile -> setAttribute ( 'tokens' , $session , Document :: SET_TYPE_APPEND );
2020-01-12 02:58:02 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> updateDocument ( $profile -> getArrayCopy ());
2020-01-05 04:45:28 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-10-31 08:53:27 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'event' , 'account.sessions.create' )
-> setParam ( 'resource' , 'users/' . $profile -> getId ())
;
2020-03-18 00:36:13 +13:00
2020-06-30 09:43:34 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2020-01-04 10:00:53 +13:00
$response
2020-06-30 09:43:34 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $profile -> getId (), $secret )]))
2020-01-12 02:58:02 +13:00
;
2020-01-04 10:00:53 +13:00
}
2020-06-30 09:43:34 +12:00
$response
2020-07-01 18:35:57 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $profile -> getId (), $secret ), $expiry , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $profile -> getId (), $secret ), $expiry , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2020-06-30 09:43:34 +12:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2020-07-03 09:48:02 +12:00
;
2020-10-31 08:53:27 +13:00
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , ( isset ( $countries [ $session -> getAttribute ( 'countryCode' )])) ? $countries [ $session -> getAttribute ( 'countryCode' )] : $locale -> getText ( 'locale.country.unknown' ))
;
2020-07-03 09:48:02 +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' )
2020-02-17 00:41:03 +13:00
-> desc ( 'Create Account Session with OAuth2' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 10:53:57 +13:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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' )
2020-01-15 05:16:24 +13:00
-> label ( 'sdk.response.code' , 301 )
-> label ( 'sdk.response.type' , 'text/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}' )
2020-09-11 02:40:14 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'providers' )), true ), 'OAuth2 Provider. Currently, supported providers are: ' . \implode ( ', ' , \array_keys ( \array_filter ( Config :: getParam ( 'providers' ), function ( $node ) { return ( ! $node [ 'mock' ]);}))) . '.' )
2020-06-30 09:43:34 +12:00
-> param ( 'success' , $oauthDefaultSuccess , function ( $clients ) { return 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' , $oauthDefaultFailure , function ( $clients ) { return 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' ])
2020-09-11 02:40:14 +12:00
-> param ( 'scopes' , [], new ArrayList ( new Text ( 128 )), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2020-06-30 09:43:34 +12:00
-> action ( function ( $provider , $success , $failure , $scopes , $request , $response , $project ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $project */
2020-01-06 00:29:42 +13:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-07-03 05:37:24 +12:00
$callback = $protocol . '://' . $request -> getHostname () . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
2020-06-30 09:43:34 +12:00
$appId = $project -> getAttribute ( 'usersOauth2' . \ucfirst ( $provider ) . 'Appid' , '' );
$appSecret = $project -> getAttribute ( 'usersOauth2' . \ucfirst ( $provider ) . 'Secret' , '{}' );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = App :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
$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 )) {
throw new Exception ( 'This provider is disabled. Please configure the provider app ID and app secret key from your ' . APP_NAME . ' console to continue.' , 412 );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$classname = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst ( $provider );
if ( ! \class_exists ( $classname )) {
throw new Exception ( 'Provider is not supported' , 501 );
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
$oauth2 = new $classname ( $appId , $appSecret , $callback , [ 'success' => $success , 'failure' => $failure ], $scopes );
$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' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 10:53:57 +13:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2020-09-11 02:40:14 +12:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project unique ID.' )
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'providers' )), true ), 'OAuth2 provider.' )
-> param ( 'code' , '' , new Text ( 1024 ), 'OAuth2 code.' )
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2020-06-30 23:09:28 +12:00
-> action ( function ( $projectId , $provider , $code , $state , $request , $response ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 23:09:28 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request -> getHostname ();
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
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 ( $protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query ([ 'project' => $projectId , 'code' => $code , 'state' => $state ]));
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' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-05-30 00:02:53 +12:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
-> label ( 'scope' , 'public' )
-> label ( 'origin' , '*' )
-> label ( 'docs' , false )
2020-09-11 02:40:14 +12:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project unique ID.' )
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'providers' )), true ), 'OAuth2 provider.' )
-> param ( 'code' , '' , new Text ( 1024 ), 'OAuth2 code.' )
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
2020-06-30 23:09:28 +12:00
-> action ( function ( $projectId , $provider , $code , $state , $request , $response ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 23:09:28 +12:00
2020-07-03 09:48:02 +12:00
$domain = $request -> getHostname ();
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
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 ( $protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query ([ 'project' => $projectId , 'code' => $code , 'state' => $state ]));
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' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 10:53:57 +13:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.sessions.create' )
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'public' )
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
-> label ( 'docs' , false )
2020-09-11 02:40:14 +12:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'providers' )), true ), 'OAuth2 provider.' )
-> param ( 'code' , '' , new Text ( 1024 ), 'OAuth2 code.' )
-> param ( 'state' , '' , new Text ( 2048 ), 'OAuth2 state params.' , true )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'geodb' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $provider , $code , $state , $request , $response , $project , $user , $projectDB , $geodb , $audits ) use ( $oauthDefaultSuccess ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-10-31 08:53:27 +13:00
/** @var MaxMind\Db\Reader $geodb */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-07-03 19:55:12 +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 ();
$appId = $project -> getAttribute ( 'usersOauth2' . \ucfirst ( $provider ) . 'Appid' , '' );
$appSecret = $project -> getAttribute ( 'usersOauth2' . \ucfirst ( $provider ) . 'Secret' , '{}' );
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = App :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
$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
$classname = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst ( $provider );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( ! \class_exists ( $classname )) {
throw new Exception ( 'Provider is not supported' , 501 );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$oauth2 = new $classname ( $appId , $appSecret , $callback );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( ! empty ( $state )) {
try {
$state = \array_merge ( $defaultState , $oauth2 -> parseState ( $state ));
} catch ( \Exception $exception ) {
throw new Exception ( '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
2020-06-30 09:43:34 +12:00
if ( ! $validateURL -> isValid ( $state [ 'success' ])) {
throw new Exception ( 'Invalid redirect URL for success login' , 400 );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( ! empty ( $state [ 'failure' ]) && ! $validateURL -> isValid ( $state [ 'failure' ])) {
throw new Exception ( 'Invalid redirect URL for failure login' , 400 );
}
$state [ 'failure' ] = null ;
$accessToken = $oauth2 -> getAccessToken ( $code );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( empty ( $accessToken )) {
if ( ! empty ( $state [ 'failure' ])) {
$response -> redirect ( $state [ 'failure' ], 301 , 0 );
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
throw new Exception ( 'Failed to obtain access token' );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$oauth2ID = $oauth2 -> getUserID ( $accessToken );
if ( empty ( $oauth2ID )) {
if ( ! empty ( $state [ 'failure' ])) {
$response -> redirect ( $state [ 'failure' ], 301 , 0 );
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
throw new Exception ( 'Missing ID from OAuth2 provider' , 400 );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$current = Auth :: tokenVerify ( $user -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( $current ) {
$projectDB -> deleteDocument ( $current ); //throw new Exception('User already logged in', 401);
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$user = ( empty ( $user -> getId ())) ? $projectDB -> getCollectionFirst ([ // Get user by provider id
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'oauth2' . \ucfirst ( $provider ) . '=' . $oauth2ID ,
],
]) : $user ;
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( empty ( $user )) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
$name = $oauth2 -> getUserName ( $accessToken );
$email = $oauth2 -> getUserEmail ( $accessToken );
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
$user = $projectDB -> getCollectionFirst ([ // Get user by provider email address
2020-01-06 00:29:42 +13:00
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
2020-06-30 09:43:34 +12:00
'email=' . $email ,
2020-01-06 00:29:42 +13:00
],
2020-01-12 02:58:02 +13:00
]);
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( ! $user || empty ( $user -> getId ())) { // Last option -> create user alone, generate random password
Authorization :: disable ();
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
try {
$user = $projectDB -> createDocument ([
'$collection' => Database :: SYSTEM_COLLECTION_USERS ,
'$permissions' => [ 'read' => [ '*' ], 'write' => [ 'user:{self}' ]],
'email' => $email ,
'emailVerification' => true ,
'status' => Auth :: USER_STATUS_ACTIVATED , // Email should already be authenticated by OAuth2 provider
'password' => Auth :: passwordHash ( Auth :: passwordGenerator ()),
2021-01-15 01:04:16 +13:00
'passwordUpdate' => \time (),
2020-06-30 09:43:34 +12:00
'registration' => \time (),
'reset' => false ,
'name' => $name ,
], [ 'email' => $email ]);
} catch ( Duplicate $th ) {
throw new Exception ( 'Account already exists' , 409 );
}
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: enable ();
2020-01-06 00:29:42 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
}
2020-06-30 09:43:34 +12:00
}
2020-01-06 00:29:42 +13:00
2020-12-28 00:57:42 +13:00
if ( Auth :: USER_STATUS_BLOCKED == $user -> getAttribute ( 'status' )) { // Account is blocked
throw new Exception ( 'Invalid credentials. User is blocked' , 401 ); // User is in status blocked
}
2020-06-30 09:43:34 +12:00
// Create session token, verify user account and update OAuth2 ID and Access Token
2020-10-31 08:53:27 +13:00
$dd = new DeviceDetector ( $request -> getUserAgent ( 'UNKNOWN' ));
$dd -> parse ();
$os = $dd -> getOs ();
$osCode = ( isset ( $os [ 'short_name' ])) ? $os [ 'short_name' ] : '' ;
$osName = ( isset ( $os [ 'name' ])) ? $os [ 'name' ] : '' ;
$osVersion = ( isset ( $os [ 'version' ])) ? $os [ 'version' ] : '' ;
$client = $dd -> getClient ();
$clientType = ( isset ( $client [ 'type' ])) ? $client [ 'type' ] : '' ;
$clientCode = ( isset ( $client [ 'short_name' ])) ? $client [ 'short_name' ] : '' ;
$clientName = ( isset ( $client [ 'name' ])) ? $client [ 'name' ] : '' ;
$clientVersion = ( isset ( $client [ 'version' ])) ? $client [ 'version' ] : '' ;
$clientEngine = ( isset ( $client [ 'engine' ])) ? $client [ 'engine' ] : '' ;
$clientEngineVersion = ( isset ( $client [ 'engine_version' ])) ? $client [ 'engine_version' ] : '' ;
2020-06-30 09:43:34 +12:00
$secret = Auth :: tokenGenerator ();
$expiry = \time () + Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
$session = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $user [ '$id' ]], 'write' => [ 'user:' . $user [ '$id' ]]],
2020-11-26 19:12:24 +13:00
'userId' => $user -> getId (),
2020-06-30 09:43:34 +12:00
'type' => Auth :: TOKEN_TYPE_LOGIN ,
2020-11-13 00:54:16 +13:00
'secret' => Auth :: hash ( $secret ), // One way hash encryption to protect DB leak
2020-06-30 09:43:34 +12:00
'expire' => $expiry ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
2020-10-31 08:53:27 +13:00
'osCode' => $osCode ,
'osName' => $osName ,
'osVersion' => $osVersion ,
'clientType' => $clientType ,
'clientCode' => $clientCode ,
'clientName' => $clientName ,
'clientVersion' => $clientVersion ,
'clientEngine' => $clientEngine ,
'clientEngineVersion' => $clientEngineVersion ,
'deviceName' => $dd -> getDeviceName (),
'deviceBrand' => $dd -> getBrandName (),
'deviceModel' => $dd -> getModel (),
2020-06-30 09:43:34 +12:00
]);
2020-10-31 08:53:27 +13:00
$record = $geodb -> get ( $request -> getIP ());
if ( $record ) {
$session
-> setAttribute ( 'countryCode' , \strtolower ( $record [ 'country' ][ 'iso_code' ]))
;
} else {
$session
-> setAttribute ( 'countryCode' , '--' )
;
}
2020-06-30 09:43:34 +12:00
$user
-> setAttribute ( 'oauth2' . \ucfirst ( $provider ), $oauth2ID )
-> setAttribute ( 'oauth2' . \ucfirst ( $provider ) . 'AccessToken' , $accessToken )
-> setAttribute ( 'status' , Auth :: USER_STATUS_ACTIVATED )
-> setAttribute ( 'tokens' , $session , Document :: SET_TYPE_APPEND )
;
Authorization :: setRole ( 'user:' . $user -> getId ());
$user = $projectDB -> updateDocument ( $user -> getArrayCopy ());
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.sessions.create' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
-> setParam ( 'data' , [ 'provider' => $provider ])
;
2020-04-09 01:38:36 +12:00
2020-06-30 09:43:34 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2020-01-06 00:29:42 +13:00
$response
2020-06-30 09:43:34 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ Auth :: $cookieName => Auth :: encodeSession ( $user -> getId (), $secret )]))
2020-01-06 00:29:42 +13:00
;
}
2020-06-30 09:43:34 +12:00
// Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing
2020-10-31 08:53:27 +13:00
if ( parse_url ( $state [ 'success' ], PHP_URL_PATH ) === parse_url ( $oauthDefaultSuccess , PHP_URL_PATH )) {
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' )
2020-07-01 18:35:57 +12:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getId (), $secret ), $expiry , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getId (), $secret ), $expiry , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2020-06-30 09:43:34 +12:00
-> redirect ( $state [ 'success' ])
;
2020-12-27 03:31:53 +13:00
});
2020-01-06 00:29:42 +13:00
2020-12-29 09:31:55 +13:00
App :: post ( '/v1/account/jwt' )
2020-10-18 06:49:09 +13:00
-> desc ( 'Create Account JWT' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'createJWT' )
-> label ( 'sdk.description' , '/docs/references/account/create-jwt.md' )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2020-12-29 06:03:27 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> action ( function ( $response , $user ) {
2020-10-18 06:49:09 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-12-29 06:03:27 +13:00
/** @var Appwrite\Database\Document $user */
$tokens = $user -> getAttribute ( 'tokens' , []);
$session = new Document ();
foreach ( $tokens as $token ) { /** @var Appwrite\Database\Document $token */
if ( $token -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
$session = $token ;
}
}
2020-10-18 06:49:09 +13:00
2020-12-29 06:03:27 +13:00
if ( $session -> isEmpty ()) {
throw new Exception ( 'No valid session found' , 401 );
}
2020-10-18 06:49:09 +13: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
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( new Document ([ 'jwt' => $jwt -> encode ([
// 'uid' => 1,
// 'aud' => 'http://site.com',
// 'scopes' => ['user'],
// 'iss' => 'http://api.mysite.com',
'userId' => $user -> getId (),
'sessionId' => $session -> getId (),
])]), Response :: MODEL_JWT );
});
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' )
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> 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 )
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-12-27 07:11:18 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $response , $user ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
2020-10-31 08:53:27 +13:00
$response -> dynamic ( $user , Response :: MODEL_USER );
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' )
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> 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 )
-> label ( 'sdk.response.model' , Response :: MODEL_ANY )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
2020-06-30 09:43:34 +12:00
-> action ( function ( $response , $user ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
2020-02-01 11:34:07 +13:00
2021-01-11 00:55:59 +13:00
$prefs = $user -> getAttribute ( 'prefs' , new \stdClass ());
2020-06-30 09:43:34 +12:00
2020-11-13 00:54:16 +13:00
$response -> dynamic ( new Document ( $prefs ), Response :: MODEL_ANY );
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' )
2020-02-01 11:34:07 +13:00
-> desc ( 'Get Account Sessions' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'getSessions' )
-> label ( 'sdk.description' , '/docs/references/account/get-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 )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'locale' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $response , $user , $locale ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
$tokens = $user -> getAttribute ( 'tokens' , []);
$sessions = [];
$countries = $locale -> getText ( 'countries' );
2020-10-31 08:53:27 +13:00
$current = Auth :: tokenVerify ( $tokens , Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret );
2020-06-30 09:43:34 +12:00
foreach ( $tokens as $token ) { /* @var $token Document */
if ( Auth :: TOKEN_TYPE_LOGIN != $token -> getAttribute ( 'type' )) {
continue ;
}
2020-02-01 11:34:07 +13:00
2020-10-31 08:53:27 +13:00
$token -> setAttribute ( 'countryName' , ( isset ( $countries [ $token -> getAttribute ( 'contryCode' )]))
? $countries [ $token -> getAttribute ( 'contryCode' )]
: $locale -> getText ( 'locale.country.unknown' ));
$token -> setAttribute ( 'current' , ( $current == $token -> getId ()) ? true : false );
2020-02-01 11:34:07 +13:00
2020-10-31 08:53:27 +13:00
$sessions [] = $token ;
2020-02-01 11:34:07 +13:00
}
2020-06-30 09:43:34 +12:00
2020-10-31 08:53:27 +13:00
$response -> dynamic ( new Document ([
'sum' => count ( $sessions ),
'sessions' => $sessions
]), 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' )
2020-02-01 11:34:07 +13:00
-> desc ( 'Get Account Logs' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-02-01 11:34:07 +13:00
-> label ( 'scope' , 'account' )
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'getLogs' )
-> label ( 'sdk.description' , '/docs/references/account/get-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 )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'register' )
-> inject ( 'project' )
-> inject ( 'user' )
-> inject ( 'locale' )
-> inject ( 'geodb' )
2020-07-03 08:24:14 +12:00
-> action ( function ( $response , $register , $project , $user , $locale , $geodb ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Locale\Locale $locale */
2020-10-25 19:15:36 +13:00
/** @var MaxMind\Db\Reader $geodb */
2020-06-30 09:43:34 +12:00
$adapter = new AuditAdapter ( $register -> get ( 'db' ));
$adapter -> setNamespace ( 'app_' . $project -> getId ());
$audit = new Audit ( $adapter );
$countries = $locale -> getText ( 'countries' );
$logs = $audit -> getLogsByUserAndActions ( $user -> getId (), [
'account.create' ,
'account.delete' ,
'account.update.name' ,
'account.update.email' ,
'account.update.password' ,
'account.update.prefs' ,
'account.sessions.create' ,
'account.sessions.delete' ,
'account.recovery.create' ,
'account.recovery.update' ,
'account.verification.create' ,
'account.verification.update' ,
'teams.membership.create' ,
'teams.membership.update' ,
'teams.membership.delete' ,
]);
$output = [];
foreach ( $logs as $i => & $log ) {
$log [ 'userAgent' ] = ( ! empty ( $log [ 'userAgent' ])) ? $log [ 'userAgent' ] : 'UNKNOWN' ;
$dd = new DeviceDetector ( $log [ 'userAgent' ]);
$dd -> skipBotDetection (); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$dd -> parse ();
2020-10-31 08:53:27 +13:00
$os = $dd -> getOs ();
$osCode = ( isset ( $os [ 'short_name' ])) ? $os [ 'short_name' ] : '' ;
$osName = ( isset ( $os [ 'name' ])) ? $os [ 'name' ] : '' ;
$osVersion = ( isset ( $os [ 'version' ])) ? $os [ 'version' ] : '' ;
$client = $dd -> getClient ();
$clientType = ( isset ( $client [ 'type' ])) ? $client [ 'type' ] : '' ;
$clientCode = ( isset ( $client [ 'short_name' ])) ? $client [ 'short_name' ] : '' ;
$clientName = ( isset ( $client [ 'name' ])) ? $client [ 'name' ] : '' ;
$clientVersion = ( isset ( $client [ 'version' ])) ? $client [ 'version' ] : '' ;
$clientEngine = ( isset ( $client [ 'engine' ])) ? $client [ 'engine' ] : '' ;
$clientEngineVersion = ( isset ( $client [ 'engine_version' ])) ? $client [ 'engine_version' ] : '' ;
$output [ $i ] = new Document ([
2020-06-30 09:43:34 +12:00
'event' => $log [ 'event' ],
'ip' => $log [ 'ip' ],
'time' => \strtotime ( $log [ 'time' ]),
2020-02-01 11:34:07 +13:00
2020-10-31 08:53:27 +13:00
'osCode' => $osCode ,
'osName' => $osName ,
'osVersion' => $osVersion ,
'clientType' => $clientType ,
'clientCode' => $clientCode ,
'clientName' => $clientName ,
'clientVersion' => $clientVersion ,
'clientEngine' => $clientEngine ,
'clientEngineVersion' => $clientEngineVersion ,
'deviceName' => $dd -> getDeviceName (),
'deviceBrand' => $dd -> getBrandName (),
'deviceModel' => $dd -> getModel (),
]);
$record = $geodb -> get ( $log [ 'ip' ]);
if ( $record ) {
$output [ $i ][ 'countryCode' ] = ( isset ( $countries [ $record [ 'country' ][ 'iso_code' ]])) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ;
$output [ $i ][ 'countryName' ] = ( isset ( $countries [ $record [ 'country' ][ 'iso_code' ]])) ? $countries [ $record [ 'country' ][ 'iso_code' ]] : $locale -> getText ( 'locale.country.unknown' );
} else {
$output [ $i ][ 'countryCode' ] = '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'locale.country.unknown' );
2020-02-01 11:34:07 +13:00
}
2020-10-31 08:53:27 +13:00
2020-02-01 11:34:07 +13:00
}
2020-06-30 09:43:34 +12:00
2020-10-31 08:53:27 +13:00
$response -> dynamic ( new Document ([ 'logs' => $output ]), Response :: MODEL_LOG_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 :: patch ( '/v1/account/name' )
2019-05-09 18:54:39 +12:00
-> desc ( 'Update Account Name' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.update.name' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-09-11 02:40:14 +12:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $name , $response , $user , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( \array_merge ( $user -> getArrayCopy (), [
'name' => $name ,
]));
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
2019-05-09 18:54:39 +12:00
}
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.update.name' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-10-31 08:53:27 +13:00
$response -> dynamic ( $user , Response :: MODEL_USER );
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' )
2019-05-09 18:54:39 +12:00
-> desc ( 'Update Account Password' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.update.password' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-09-11 02:40:14 +12:00
-> param ( 'password' , '' , new Password (), 'New user password. Must be between 6 to 32 chars.' )
-> param ( 'oldPassword' , '' , new Password (), 'Old user password. Must be between 6 to 32 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $password , $oldPassword , $response , $user , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
if ( ! Auth :: passwordVerify ( $oldPassword , $user -> getAttribute ( 'password' ))) { // Double check user password
throw new Exception ( 'Invalid credentials' , 401 );
}
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( \array_merge ( $user -> getArrayCopy (), [
'password' => Auth :: passwordHash ( $password ),
]));
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
2019-05-09 18:54:39 +12:00
}
2020-06-30 09:43:34 +12:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.update.password' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-10-31 08:53:27 +13:00
$response -> dynamic ( $user , Response :: MODEL_USER );
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' )
2019-05-09 18:54:39 +12:00
-> desc ( 'Update Account Email' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.update.email' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'sdk.response.model' , Response :: MODEL_USER )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
-> param ( 'password' , '' , new Password (), 'User password. Must be between 6 to 32 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-10-31 08:53:27 +13:00
-> action ( function ( $email , $password , $response , $user , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
if ( ! Auth :: passwordVerify ( $password , $user -> getAttribute ( 'password' ))) { // Double check user password
throw new Exception ( 'Invalid credentials' , 401 );
}
2019-07-21 23:43:06 +12:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
if ( ! empty ( $profile )) {
throw new Exception ( 'User already registered' , 400 );
}
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
// TODO after this user needs to confirm mail again
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( \array_merge ( $user -> getArrayCopy (), [
'email' => $email ,
'emailVerification' => false ,
]));
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
2019-05-09 18:54:39 +12:00
}
2020-10-31 08:53:27 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.update.email' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-10-31 08:53:27 +13:00
$response -> dynamic ( $user , Response :: MODEL_USER );
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/prefs' )
2020-01-06 00:29:42 +13:00
-> desc ( 'Update Account Preferences' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.update.prefs' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'sdk.response.model' , Response :: MODEL_ANY )
2020-10-31 08:53:27 +13:00
-> param ( 'prefs' , [], new Assoc (), 'Prefs key-value JSON object.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-07-06 02:19:59 +12:00
-> action ( function ( $prefs , $response , $user , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-10-31 08:53:27 +13:00
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( \array_merge ( $user -> getArrayCopy (), [
2020-10-31 08:53:27 +13:00
'prefs' => $prefs ,
2020-06-30 09:43:34 +12:00
]));
2019-05-09 18:54:39 +12:00
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2019-05-09 18:54:39 +12:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'event' , 'account.update.prefs' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-01-12 02:58:02 +13:00
2020-11-24 04:04:02 +13:00
$response -> dynamic ( $user , Response :: MODEL_USER );
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 :: delete ( '/v1/account' )
2019-05-09 18:54:39 +12:00
-> desc ( 'Delete Account' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.delete' )
2019-05-09 18:54:39 +12:00
-> label ( 'scope' , 'account' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
2019-05-09 18:54:39 +12:00
-> label ( 'sdk.namespace' , 'account' )
-> label ( 'sdk.method' , 'delete' )
2019-10-08 20:21:54 +13:00
-> label ( 'sdk.description' , '/docs/references/account/delete.md' )
2020-11-12 10:02:24 +13:00
-> label ( 'sdk.response.code' , Response :: STATUS_CODE_NOCONTENT )
-> label ( 'sdk.response.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
-> inject ( 'events' )
2020-12-07 11:14:57 +13:00
-> action ( function ( $request , $response , $user , $projectDB , $audits , $events ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( \array_merge ( $user -> getArrayCopy (), [
'status' => Auth :: USER_STATUS_BLOCKED ,
]));
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-03-18 00:36:13 +13:00
2020-06-30 09:43:34 +12:00
//TODO delete all tokens or only current session?
//TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later
/*
* Data to delete
* * Tokens
* * Memberships
*/
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.delete' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
-> setParam ( 'data' , $user -> getArrayCopy ())
;
2020-12-07 11:14:57 +13:00
$events
2020-10-31 08:53:27 +13:00
-> setParam ( 'payload' , $response -> output ( $user , Response :: MODEL_USER ))
2020-06-30 09:43:34 +12:00
;
if ( ! Config :: getParam ( 'domainVerification' )) {
2019-05-09 18:54:39 +12:00
$response
2020-06-30 09:43:34 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]))
2020-01-12 02:58:02 +13:00
;
2019-05-09 18:54:39 +12:00
}
2020-06-30 09:43:34 +12:00
$response
2020-07-01 18:35:57 +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-06-30 09:43:34 +12:00
-> noContent ()
;
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' )
2020-01-06 00:29:42 +13:00
-> desc ( 'Delete Account Session' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-06 00:29:42 +13:00
-> label ( 'scope' , 'account' )
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.sessions.delete' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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.type' , Response :: CONTENT_TYPE_JSON )
-> label ( 'sdk.response.model' , Response :: MODEL_NONE )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 100 )
2020-09-11 02:40:14 +12:00
-> param ( 'sessionId' , null , new UID (), 'Session unique ID. Use the string \'current\' to delete the current device session.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
-> inject ( 'events' )
2020-12-07 11:14:57 +13:00
-> action ( function ( $sessionId , $request , $response , $user , $projectDB , $audits , $events ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-06-30 09:43:34 +12:00
$sessionId = ( $sessionId === 'current' )
? Auth :: tokenVerify ( $user -> getAttribute ( 'tokens' ), Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret )
: $sessionId ;
$tokens = $user -> getAttribute ( 'tokens' , []);
2020-01-24 11:33:44 +13:00
2020-06-30 09:43:34 +12:00
foreach ( $tokens as $token ) { /* @var $token Document */
if (( $sessionId == $token -> getId ()) && Auth :: TOKEN_TYPE_LOGIN == $token -> getAttribute ( 'type' )) {
2020-02-17 20:16:11 +13:00
if ( ! $projectDB -> deleteDocument ( $token -> getId ())) {
2020-01-24 11:33:44 +13:00
throw new Exception ( 'Failed to remove token from DB' , 500 );
}
2020-07-06 02:19:59 +12:00
$audits
2020-02-17 20:16:11 +13:00
-> setParam ( 'userId' , $user -> getId ())
2020-01-24 11:33:44 +13:00
-> setParam ( 'event' , 'account.sessions.delete' )
2020-02-17 20:16:11 +13:00
-> setParam ( 'resource' , '/user/' . $user -> getId ())
2020-01-24 11:33:44 +13:00
;
2020-06-25 08:59:04 +12:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2020-03-18 00:36:13 +13:00
$response
2020-06-20 23:20:49 +12:00
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]))
2020-03-18 00:36:13 +13:00
;
}
2020-11-21 10:02:26 +13:00
$token -> setAttribute ( 'current' , false );
2020-03-18 00:36:13 +13:00
2020-01-24 11:33:44 +13:00
if ( $token -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
2020-11-21 10:02:26 +13:00
$token -> setAttribute ( 'current' , true );
2020-01-24 11:33:44 +13:00
$response
2020-07-01 18:35:57 +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
;
}
2020-06-30 09:43:34 +12:00
2020-12-07 11:14:57 +13:00
$events
2020-11-21 10:02:26 +13:00
-> setParam ( 'payload' , $response -> output ( $token , Response :: MODEL_SESSION ))
;
2020-06-30 09:43:34 +12:00
return $response -> noContent ();
}
}
throw new Exception ( 'Session not found' , 404 );
2020-12-27 03:31:53 +13:00
});
2020-06-30 09:43:34 +12:00
App :: delete ( '/v1/account/sessions' )
-> desc ( 'Delete All Account Sessions' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2020-10-31 08:53:27 +13:00
-> label ( 'event' , 'account.sessions.delete' )
2020-06-30 09:43:34 +12:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
-> 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.type' , Response :: CONTENT_TYPE_JSON )
-> 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' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
-> inject ( 'events' )
2020-12-07 11:14:57 +13:00
-> action ( function ( $request , $response , $user , $projectDB , $audits , $events ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2020-06-30 09:43:34 +12:00
2020-06-30 23:09:28 +12:00
$protocol = $request -> getProtocol ();
2020-06-30 09:43:34 +12:00
$tokens = $user -> getAttribute ( 'tokens' , []);
foreach ( $tokens as $token ) { /* @var $token Document */
if ( ! $projectDB -> deleteDocument ( $token -> getId ())) {
throw new Exception ( 'Failed to remove token from DB' , 500 );
2020-01-24 11:33:44 +13:00
}
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.sessions.delete' )
-> setParam ( 'resource' , '/user/' . $user -> getId ())
;
if ( ! Config :: getParam ( 'domainVerification' )) {
$response
-> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]))
;
}
2020-11-21 10:02:26 +13:00
$token -> setAttribute ( 'current' , false );
2020-06-30 09:43:34 +12:00
if ( $token -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
2020-11-21 10:02:26 +13:00
$token -> setAttribute ( 'current' , true );
2020-06-30 09:43:34 +12:00
$response
2020-07-01 18:35:57 +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-06-30 09:43:34 +12:00
;
}
2020-01-24 11:33:44 +13:00
}
2020-11-21 10:02:26 +13:00
2020-12-07 11:14:57 +13:00
$events
2020-11-21 10:02:26 +13:00
-> setParam ( 'payload' , $response -> output ( new Document ([
'sum' => count ( $tokens ),
'sessions' => $tokens
]), Response :: MODEL_SESSION_LIST ))
;
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' )
2020-11-19 08:38:31 +13:00
-> label ( 'event' , 'account.recovery.create' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-09-11 02:40:14 +12:00
-> param ( 'email' , '' , new Email (), 'User email.' )
2020-06-30 09:43:34 +12:00
-> param ( 'url' , '' , function ( $clients ) { return 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' )
-> inject ( 'projectDB' )
-> inject ( 'project' )
-> inject ( 'locale' )
-> inject ( 'mails' )
-> inject ( 'audits' )
-> inject ( 'events' )
2020-12-07 11:14:57 +13:00
-> action ( function ( $email , $url , $request , $response , $projectDB , $project , $locale , $mails , $audits , $events ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Database $projectDB */
/** @var Appwrite\Database\Document $project */
/** @var Utopia\Locale\Locale $locale */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $mails */
/** @var Appwrite\Event\Event $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2020-11-21 01:35:16 +13:00
$isPreviliggedUser = Auth :: isPreviliggedUser ( Authorization :: $roles );
$isAppUser = Auth :: isAppUser ( Authorization :: $roles );
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
2020-01-12 02:58:02 +13:00
2020-12-28 00:57:42 +13:00
if ( Auth :: USER_STATUS_BLOCKED == $profile -> getAttribute ( 'status' )) { // Account is blocked
throw new Exception ( 'Invalid credentials. User is blocked' , 401 ); // User is in status blocked
}
2020-06-30 09:43:34 +12:00
$secret = Auth :: tokenGenerator ();
$recovery = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $profile -> getId ()], 'write' => [ 'user:' . $profile -> getId ()]],
2020-11-26 19:12:24 +13:00
'userId' => $profile -> getId (),
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
2020-06-30 09:43:34 +12:00
'expire' => \time () + Auth :: TOKEN_EXPIRATION_RECOVERY ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
]);
Authorization :: setRole ( 'user:' . $profile -> getId ());
2020-01-12 02:58:02 +13:00
2020-06-30 09:43:34 +12:00
$recovery = $projectDB -> createDocument ( $recovery -> getArrayCopy ());
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $recovery ) {
throw new Exception ( 'Failed saving recovery to DB' , 500 );
}
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$profile -> setAttribute ( 'tokens' , $recovery , Document :: SET_TYPE_APPEND );
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> updateDocument ( $profile -> getArrayCopy ());
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $profile ) {
throw new Exception ( 'Failed to save user to DB' , 500 );
2020-01-06 12:07:41 +13:00
}
2020-06-30 09:43:34 +12:00
$url = Template :: parseURL ( $url );
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $profile -> getId (), 'secret' => $secret ]);
$url = Template :: unParseURL ( $url );
2020-06-30 17:27:52 +12:00
$body = new Template ( __DIR__ . '/../../config/locale/templates/email-base.tpl' );
$content = new Template ( __DIR__ . '/../../config/locale/translations/templates/' . $locale -> getText ( 'account.emails.recovery.body' ));
$cta = new Template ( __DIR__ . '/../../config/locale/templates/email-cta.tpl' );
2020-06-30 09:43:34 +12:00
$body
-> setParam ( '{{content}}' , $content -> render ())
-> setParam ( '{{cta}}' , $cta -> render ())
-> setParam ( '{{title}}' , $locale -> getText ( 'account.emails.recovery.title' ))
-> setParam ( '{{direction}}' , $locale -> getText ( 'settings.direction' ))
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' , [ '[APP-NAME]' ]))
-> setParam ( '{{name}}' , $profile -> getAttribute ( 'name' ))
-> setParam ( '{{redirect}}' , $url )
-> setParam ( '{{bg-body}}' , '#f6f6f6' )
-> setParam ( '{{bg-content}}' , '#ffffff' )
-> setParam ( '{{bg-cta}}' , '#3498db' )
-> setParam ( '{{bg-cta-hover}}' , '#34495e' )
-> setParam ( '{{text-content}}' , '#000000' )
-> setParam ( '{{text-cta}}' , '#ffffff' )
;
2020-07-06 02:19:59 +12:00
$mails
2020-06-30 09:43:34 +12:00
-> setParam ( 'event' , 'account.recovery.create' )
2020-07-06 09:54:41 +12:00
-> setParam ( 'from' , ( $project -> getId () === 'console' ) ? '' : \sprintf ( $locale -> getText ( 'account.emails.team' ), $project -> getAttribute ( 'name' )))
2020-06-30 09:43:34 +12:00
-> setParam ( 'recipient' , $profile -> getAttribute ( 'email' , '' ))
-> setParam ( 'name' , $profile -> getAttribute ( 'name' , '' ))
-> setParam ( 'subject' , $locale -> getText ( 'account.emails.recovery.title' ))
-> setParam ( 'body' , $body -> render ())
-> trigger ();
;
2020-12-07 11:14:57 +13:00
$events
2020-11-19 08:38:31 +13:00
-> setParam ( 'payload' ,
$response -> output ( $recovery -> setAttribute ( 'secret' , $secret ),
Response :: MODEL_TOKEN
))
;
$recovery // Hide secret for clients, sp
-> setAttribute ( 'secret' ,
2020-11-21 01:35:16 +13:00
( $isPreviliggedUser || $isAppUser ) ? $secret : '' );
2020-11-19 08:38:31 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'event' , 'account.recovery.create' )
-> setParam ( 'resource' , 'users/' . $profile -> getId ())
;
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2020-11-13 00:54:16 +13:00
-> dynamic ( $recovery , Response :: MODEL_TOKEN )
2020-06-30 09:43:34 +12:00
;
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' )
2020-02-09 07:51:49 +13:00
-> desc ( 'Complete Password Recovery' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-06 12:07:41 +13:00
-> label ( 'scope' , 'public' )
2020-11-19 08:38:31 +13:00
-> label ( 'event' , 'account.recovery.update' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
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}' )
2020-09-11 02:40:14 +12:00
-> param ( 'userId' , '' , new UID (), 'User account UID address.' )
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid reset token.' )
-> param ( 'password' , '' , new Password (), 'New password. Must be between 6 to 32 chars.' )
-> param ( 'passwordAgain' , '' , new Password (), 'New password again. Must be between 6 to 32 chars.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-07-06 02:19:59 +12:00
-> action ( function ( $userId , $secret , $password , $passwordAgain , $response , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
if ( $password !== $passwordAgain ) {
throw new Exception ( 'Passwords must match' , 400 );
}
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'$id=' . $userId ,
],
]);
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$recovery = Auth :: tokenVerify ( $profile -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_RECOVERY , $secret );
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if ( ! $recovery ) {
throw new Exception ( 'Invalid recovery token' , 401 );
}
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: setRole ( 'user:' . $profile -> getId ());
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> updateDocument ( \array_merge ( $profile -> getArrayCopy (), [
'password' => Auth :: passwordHash ( $password ),
2021-01-15 01:04:16 +13:00
'passwordUpdate' => \time (),
2020-06-30 09:43:34 +12:00
'emailVerification' => true ,
]));
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
/**
* We act like we ' re updating and validating
* the recovery token but actually we don ' t need it anymore .
*/
if ( ! $projectDB -> deleteDocument ( $recovery )) {
throw new Exception ( 'Failed to remove recovery from DB' , 500 );
}
2020-01-06 12:07:41 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'event' , 'account.recovery.update' )
-> setParam ( 'resource' , 'users/' . $profile -> getId ())
;
2020-01-06 12:07:41 +13:00
2020-06-30 09:43:34 +12:00
$recovery = $profile -> search ( '$id' , $recovery , $profile -> getAttribute ( 'tokens' , []));
2020-01-12 02:58:02 +13:00
2020-11-13 00:54:16 +13:00
$response -> dynamic ( $recovery , 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' )
2020-11-21 10:02:26 +13:00
-> label ( 'event' , 'account.verification.create' )
2020-02-08 11:46:12 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'createVerification' )
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create-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 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-06-30 09:43:34 +12:00
-> param ( 'url' , '' , function ( $clients ) { return 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' )
-> inject ( 'projectDB' )
-> inject ( 'locale' )
-> inject ( 'audits' )
-> inject ( 'events' )
-> inject ( 'mails' )
2020-12-07 11:14:57 +13:00
-> action ( function ( $url , $request , $response , $project , $user , $projectDB , $locale , $audits , $events , $mails ) {
2020-10-30 02:07:56 +13:00
/** @var Utopia\Swoole\Request $request */
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
/** @var Utopia\Locale\Locale $locale */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-12-07 11:14:57 +13:00
/** @var Appwrite\Event\Event $events */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $mails */
2020-11-21 01:35:16 +13:00
$isPreviliggedUser = Auth :: isPreviliggedUser ( Authorization :: $roles );
$isAppUser = Auth :: isAppUser ( Authorization :: $roles );
2020-06-30 09:43:34 +12:00
$verificationSecret = Auth :: tokenGenerator ();
$verification = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $user -> getId ()], 'write' => [ 'user:' . $user -> getId ()]],
2020-11-26 19:12:24 +13:00
'userId' => $user -> getId (),
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
2020-06-30 09:43:34 +12:00
'expire' => \time () + Auth :: TOKEN_EXPIRATION_CONFIRM ,
2020-07-04 03:14:51 +12:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-30 09:43:34 +12:00
'ip' => $request -> getIP (),
]);
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: setRole ( 'user:' . $user -> getId ());
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$verification = $projectDB -> createDocument ( $verification -> getArrayCopy ());
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $verification ) {
throw new Exception ( 'Failed saving verification to DB' , 500 );
}
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$user -> setAttribute ( 'tokens' , $verification , Document :: SET_TYPE_APPEND );
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$user = $projectDB -> updateDocument ( $user -> getArrayCopy ());
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $user ) {
throw new Exception ( 'Failed to save user to DB' , 500 );
2020-01-12 13:20:35 +13:00
}
2020-06-30 09:43:34 +12:00
$url = Template :: parseURL ( $url );
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getId (), 'secret' => $verificationSecret ]);
$url = Template :: unParseURL ( $url );
2020-06-30 17:27:52 +12:00
$body = new Template ( __DIR__ . '/../../config/locale/templates/email-base.tpl' );
$content = new Template ( __DIR__ . '/../../config/locale/translations/templates/' . $locale -> getText ( 'account.emails.verification.body' ));
$cta = new Template ( __DIR__ . '/../../config/locale/templates/email-cta.tpl' );
2020-06-30 09:43:34 +12:00
$body
-> setParam ( '{{content}}' , $content -> render ())
-> setParam ( '{{cta}}' , $cta -> render ())
-> setParam ( '{{title}}' , $locale -> getText ( 'account.emails.verification.title' ))
-> setParam ( '{{direction}}' , $locale -> getText ( 'settings.direction' ))
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' , [ '[APP-NAME]' ]))
-> setParam ( '{{name}}' , $user -> getAttribute ( 'name' ))
-> setParam ( '{{redirect}}' , $url )
-> setParam ( '{{bg-body}}' , '#f6f6f6' )
-> setParam ( '{{bg-content}}' , '#ffffff' )
-> setParam ( '{{bg-cta}}' , '#3498db' )
-> setParam ( '{{bg-cta-hover}}' , '#34495e' )
-> setParam ( '{{text-content}}' , '#000000' )
-> setParam ( '{{text-cta}}' , '#ffffff' )
;
2020-07-06 02:19:59 +12:00
$mails
2020-06-30 09:43:34 +12:00
-> setParam ( 'event' , 'account.verification.create' )
2020-07-06 09:54:41 +12:00
-> setParam ( 'from' , ( $project -> getId () === 'console' ) ? '' : \sprintf ( $locale -> getText ( 'account.emails.team' ), $project -> getAttribute ( 'name' )))
2020-06-30 09:43:34 +12:00
-> setParam ( 'recipient' , $user -> getAttribute ( 'email' ))
-> setParam ( 'name' , $user -> getAttribute ( 'name' ))
-> setParam ( 'subject' , $locale -> getText ( 'account.emails.verification.title' ))
-> setParam ( 'body' , $body -> render ())
-> trigger ()
;
2020-12-07 11:14:57 +13:00
$events
2020-11-19 08:38:31 +13:00
-> setParam ( 'payload' ,
$response -> output ( $verification -> setAttribute ( 'secret' , $verificationSecret ),
Response :: MODEL_TOKEN
))
;
$verification // Hide secret for clients, sp
-> setAttribute ( 'secret' ,
2020-11-21 01:35:16 +13:00
( $isPreviliggedUser || $isAppUser ) ? $verificationSecret : '' );
2020-11-19 08:38:31 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'event' , 'account.verification.create' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2020-11-13 00:54:16 +13:00
-> dynamic ( $verification , Response :: MODEL_TOKEN )
2020-06-30 09:43:34 +12:00
;
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' )
2020-02-10 10:37:28 +13:00
-> desc ( 'Complete Email Verification' )
2020-06-26 06:32:12 +12:00
-> groups ([ 'api' , 'account' ])
2020-01-12 13:20:35 +13:00
-> label ( 'scope' , 'public' )
2020-11-21 10:02:26 +13:00
-> label ( 'event' , 'account.verification.update' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'updateVerification' )
2020-01-12 13:20:35 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-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}' )
2020-09-11 02:40:14 +12:00
-> param ( 'userId' , '' , new UID (), 'User unique ID.' )
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
2020-12-27 03:31:53 +13:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'projectDB' )
-> inject ( 'audits' )
2020-07-06 02:19:59 +12:00
-> action ( function ( $userId , $secret , $response , $user , $projectDB , $audits ) {
2020-10-30 02:50:49 +13:00
/** @var Appwrite\Utopia\Response $response */
2020-06-30 09:43:34 +12:00
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Database\Database $projectDB */
2020-07-06 02:19:59 +12:00
/** @var Appwrite\Event\Event $audits */
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> getCollectionFirst ([ // Get user by email address
'limit' => 1 ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'$id=' . $userId ,
],
]);
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$verification = Auth :: tokenVerify ( $profile -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_VERIFICATION , $secret );
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
if ( ! $verification ) {
throw new Exception ( 'Invalid verification token' , 401 );
}
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
Authorization :: setRole ( 'user:' . $profile -> getId ());
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$profile = $projectDB -> updateDocument ( \array_merge ( $profile -> getArrayCopy (), [
'emailVerification' => true ,
]));
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
/**
* We act like we ' re updating and validating
* the verification token but actually we don ' t need it anymore .
*/
if ( ! $projectDB -> deleteDocument ( $verification )) {
throw new Exception ( 'Failed to remove verification from DB' , 500 );
}
2020-01-12 13:20:35 +13:00
2020-07-06 02:19:59 +12:00
$audits
2020-06-30 09:43:34 +12:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'event' , 'account.verification.update' )
-> setParam ( 'resource' , 'users/' . $user -> getId ())
;
2020-01-12 13:20:35 +13:00
2020-06-30 09:43:34 +12:00
$verification = $profile -> search ( '$id' , $verification , $profile -> getAttribute ( 'tokens' , []));
2020-01-12 13:20:35 +13:00
2020-11-13 00:54:16 +13:00
$response -> dynamic ( $verification , Response :: MODEL_TOKEN );
2020-12-27 03:31:53 +13:00
});