2019-05-09 18:54:39 +12:00
< ? php
2020-01-12 02:58:02 +13:00
global $utopia , $register , $request , $response , $user , $audit ,
$webhook , $project , $domain , $projectDB , $providers , $clients ;
2019-05-09 18:54:39 +12:00
use Utopia\Exception ;
2020-01-12 02:58:02 +13:00
use Utopia\Response ;
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 ;
2019-11-09 04:27:16 +13:00
use Utopia\Locale\Locale ;
2019-05-09 18:54:39 +12:00
use Auth\Auth ;
use Auth\Validator\Password ;
2019-07-21 23:43:06 +12:00
use Database\Database ;
2020-01-04 10:00:53 +13:00
use Database\Document ;
use Database\Validator\UID ;
2019-05-09 18:54:39 +12:00
use Database\Validator\Authorization ;
use DeviceDetector\DeviceDetector ;
use GeoIp2\Database\Reader ;
2020-01-04 10:00:53 +13:00
use Template\Template ;
use OpenSSL\OpenSSL ;
2019-05-09 18:54:39 +12:00
2019-12-17 08:35:33 +13:00
include_once __DIR__ . '/../shared/api.php' ;
2019-11-30 07:26:06 +13:00
2020-01-12 02:58:02 +13:00
$oauthKeys = [];
$utopia -> init ( function () use ( $providers , & $oauthKeys ) {
foreach ( $providers as $key => $provider ) {
if ( ! $provider [ 'enabled' ]) {
continue ;
}
$oauthKeys [] = 'oauth' . ucfirst ( $key );
$oauthKeys [] = 'oauth' . ucfirst ( $key ) . 'AccessToken' ;
}
});
2020-01-04 10:00:53 +13:00
$utopia -> post ( '/v1/account' )
2020-01-23 12:32:10 +13:00
-> desc ( 'Create Account' )
2020-01-04 10:00:53 +13:00
-> label ( 'webhook' , '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-01-04 10:00:53 +13:00
-> label ( 'abuse-limit' , 10 )
2020-02-10 18:58:29 +13:00
-> param ( 'email' , '' , function () { return new Email (); }, 'User email.' )
2020-02-10 10:37:28 +13:00
-> param ( 'password' , '' , function () { return new Password (); }, 'User password.' )
-> param ( 'name' , '' , function () { return new Text ( 100 ); }, 'User name.' , true )
2020-01-04 10:00:53 +13:00
-> action (
2020-01-12 02:58:02 +13:00
function ( $email , $password , $name ) use ( $register , $request , $response , $audit , $projectDB , $project , $webhook , $oauthKeys ) {
2020-01-04 10:00:53 +13:00
if ( 'console' === $project -> getUid ()) {
$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 );
}
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 );
}
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 );
}
}
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
if ( ! empty ( $profile )) {
throw new Exception ( 'Account already exists' , 409 );
}
Authorization :: disable ();
$user = $projectDB -> createDocument ([
'$collection' => Database :: SYSTEM_COLLECTION_USERS ,
'$permissions' => [
'read' => [ '*' ],
'write' => [ 'user:{self}' ],
],
'email' => $email ,
2020-02-10 10:37:28 +13:00
'emailVerification' => false ,
2020-01-04 10:00:53 +13:00
'status' => Auth :: USER_STATUS_UNACTIVATED ,
'password' => Auth :: passwordHash ( $password ),
'password-update' => time (),
'registration' => time (),
'reset' => false ,
'name' => $name ,
]);
Authorization :: enable ();
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-05 04:45:28 +13:00
$webhook
-> setParam ( 'payload' , [
'name' => $name ,
'email' => $email ,
])
;
2020-01-04 10:00:53 +13:00
2020-01-05 04:45:28 +13:00
$audit
-> setParam ( 'userId' , $user -> getUid ())
-> setParam ( 'event' , 'account.create' )
2020-01-06 00:29:42 +13:00
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
2020-01-04 10:00:53 +13:00
;
2020-01-12 02:58:02 +13:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> json ( array_merge ( $user -> getArrayCopy ( array_merge (
[
'$uid' ,
'email' ,
'registration' ,
'name' ,
],
$oauthKeys
)), [ 'roles' => Authorization :: getRoles ()]));
2020-01-05 04:45:28 +13:00
}
);
$utopia -> post ( '/v1/account/sessions' )
2020-01-06 00:29:42 +13:00
-> desc ( 'Create Account Session' )
2020-01-05 04:45:28 +13:00
-> label ( 'webhook' , '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-01-05 04:45:28 +13:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-02-10 18:58:29 +13:00
-> param ( 'email' , '' , function () { return new Email (); }, 'User email.' )
-> param ( 'password' , '' , function () { return new Password (); }, 'User password.' )
2020-01-05 04:45:28 +13:00
-> action (
function ( $email , $password ) use ( $response , $request , $projectDB , $audit , $webhook ) {
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
2020-01-12 02:58:02 +13:00
if ( false == $profile || ! Auth :: passwordVerify ( $password , $profile -> getAttribute ( 'password' ))) {
2020-01-05 04:45:28 +13:00
$audit
//->setParam('userId', $profile->getUid())
2020-01-06 00:29:42 +13:00
-> setParam ( 'event' , 'account.sesssions.failed' )
2020-01-12 02:58:02 +13:00
-> setParam ( 'resource' , 'users/' . ( $profile ? $profile -> getUid () : '' ))
2020-01-05 04:45:28 +13:00
;
2020-01-04 10:00:53 +13:00
2020-01-05 04:45:28 +13:00
throw new Exception ( 'Invalid credentials' , 401 ); // Wrong password or username
}
2020-01-04 10:00:53 +13:00
2020-01-05 04:45:28 +13:00
$expiry = time () + Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
$secret = Auth :: tokenGenerator ();
2020-01-12 02:58:02 +13:00
$session = new Document ([
2020-01-05 04:45:28 +13:00
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $profile -> getUid ()], 'write' => [ 'user:' . $profile -> getUid ()]],
'type' => Auth :: TOKEN_TYPE_LOGIN ,
'secret' => Auth :: hash ( $secret ), // On way hash encryption to protect DB leak
'expire' => $expiry ,
'userAgent' => $request -> getServer ( 'HTTP_USER_AGENT' , 'UNKNOWN' ),
'ip' => $request -> getIP (),
2020-01-12 02:58:02 +13:00
]);
2020-01-05 04:45:28 +13:00
Authorization :: setRole ( 'user:' . $profile -> getUid ());
2020-01-04 10:00:53 +13:00
2020-01-12 02:58:02 +13:00
$session = $projectDB -> createDocument ( $session -> getArrayCopy ());
if ( false === $session ) {
throw new Exception ( 'Failed saving session to DB' , 500 );
}
$profile -> setAttribute ( 'tokens' , $session , Document :: SET_TYPE_APPEND );
2020-01-05 04:45:28 +13:00
$profile = $projectDB -> updateDocument ( $profile -> getArrayCopy ());
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
2020-01-04 10:00:53 +13:00
}
$webhook
-> setParam ( 'payload' , [
2020-01-05 04:45:28 +13:00
'name' => $profile -> getAttribute ( 'name' , '' ),
'email' => $profile -> getAttribute ( 'email' , '' ),
2020-01-04 10:00:53 +13:00
])
;
$audit
2020-01-05 04:45:28 +13:00
-> setParam ( 'userId' , $profile -> getUid ())
2020-01-12 02:58:02 +13:00
-> setParam ( 'event' , 'account.sessions.create' )
2020-01-06 00:29:42 +13:00
-> setParam ( 'resource' , 'users/' . $profile -> getUid ())
2020-01-04 10:00:53 +13:00
;
2020-01-12 02:58:02 +13:00
2020-01-04 10:00:53 +13:00
$response
2020-01-15 09:50:49 +13:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $profile -> getUid (), $secret ), $expiry , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
2020-01-15 10:11:02 +13:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $profile -> getUid (), $secret ), $expiry , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
2020-01-12 02:58:02 +13:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> json ( $session -> getArrayCopy ([ '$uid' , 'type' , 'expire' ]))
;
2020-01-04 10:00:53 +13:00
}
);
2020-01-06 00:29:42 +13:00
$utopia -> get ( '/v1/account/sessions/oauth/:provider' )
-> desc ( 'Create Account Session with OAuth' )
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-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'createOAuthSession' )
2020-01-06 00:29:42 +13:00
-> label ( 'sdk.description' , '/docs/references/account/create-session-oauth.md' )
2020-01-15 05:16:24 +13:00
-> label ( 'sdk.response.code' , 301 )
-> label ( 'sdk.response.type' , 'text/html' )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
2020-02-10 18:54:26 +13:00
-> param ( 'provider' , '' , function () use ( $providers ) { return new WhiteList ( array_keys ( $providers )); }, 'OAuth Provider. Currently, supported providers are: ' . implode ( ', ' , array_keys ( array_filter ( $providers , function ( $node ) { return ( ! $node [ 'mock' ]);}))) . '.' )
2020-01-06 00:29:42 +13:00
-> param ( 'success' , '' , function () use ( $clients ) { return new Host ( $clients ); }, 'URL to redirect back to your app after a successful login attempt.' )
-> param ( 'failure' , '' , function () use ( $clients ) { return new Host ( $clients ); }, 'URL to redirect back to your app after a failed login attempt.' )
-> action (
function ( $provider , $success , $failure ) use ( $response , $request , $project ) {
$callback = $request -> getServer ( 'REQUEST_SCHEME' , 'https' ) . '://' . $request -> getServer ( 'HTTP_HOST' ) . '/v1/account/sessions/oauth/callback/' . $provider . '/' . $project -> getUid ();
$appId = $project -> getAttribute ( 'usersOauth' . ucfirst ( $provider ) . 'Appid' , '' );
$appSecret = $project -> getAttribute ( 'usersOauth' . ucfirst ( $provider ) . 'Secret' , '{}' );
$appSecret = json_decode ( $appSecret , true );
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = $request -> getServer ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , hex2bin ( $appSecret [ 'iv' ]), hex2bin ( $appSecret [ 'tag' ]));
}
if ( empty ( $appId ) || empty ( $appSecret )) {
throw new Exception ( 'Provider is undefined, configure provider app ID and app secret key to continue' , 412 );
}
$classname = 'Auth\\OAuth\\' . ucfirst ( $provider );
if ( ! class_exists ( $classname )) {
throw new Exception ( 'Provider is not supported' , 501 );
}
$oauth = new $classname ( $appId , $appSecret , $callback , [ 'success' => $success , 'failure' => $failure ]);
$response -> redirect ( $oauth -> getLoginURL ());
}
);
$utopia -> get ( '/v1/account/sessions/oauth/callback/:provider/:projectId' )
-> desc ( 'OAuth Callback' )
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-02-10 18:54:26 +13:00
-> param ( 'projectId' , '' , function () { return new Text ( 1024 ); }, 'Project unique ID.' )
-> param ( 'provider' , '' , function () use ( $providers ) { return new WhiteList ( array_keys ( $providers )); }, 'OAuth provider.' )
-> param ( 'code' , '' , function () { return new Text ( 1024 ); }, 'OAuth code.' )
-> param ( 'state' , '' , function () { return new Text ( 2048 ); }, 'Login state params.' , true )
2020-01-06 00:29:42 +13:00
-> action (
function ( $projectId , $provider , $code , $state ) use ( $response , $request , $domain ) {
$response -> redirect ( $request -> getServer ( 'REQUEST_SCHEME' , 'https' ) . '://' . $domain . '/v1/account/sessions/oauth/' . $provider . '/redirect?'
. http_build_query ([ 'project' => $projectId , 'code' => $code , 'state' => $state ]));
}
);
$utopia -> get ( '/v1/account/sessions/oauth/:provider/redirect' )
-> desc ( 'OAuth Redirect' )
2020-01-12 10:53:57 +13:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-06 00:29:42 +13:00
-> label ( 'webhook' , 'account.sessions.create' )
-> label ( 'scope' , 'public' )
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
-> label ( 'docs' , false )
2020-02-10 18:54:26 +13:00
-> param ( 'provider' , '' , function () use ( $providers ) { return new WhiteList ( array_keys ( $providers )); }, 'OAuth provider.' )
-> param ( 'code' , '' , function () { return new Text ( 1024 ); }, 'OAuth code.' )
-> param ( 'state' , '' , function () { return new Text ( 2048 ); }, 'OAuth state params.' , true )
2020-01-06 00:29:42 +13:00
-> action (
function ( $provider , $code , $state ) use ( $response , $request , $user , $projectDB , $project , $audit ) {
$callback = $request -> getServer ( 'REQUEST_SCHEME' , 'https' ) . '://' . $request -> getServer ( 'HTTP_HOST' ) . '/v1/account/sessions/oauth/callback/' . $provider . '/' . $project -> getUid ();
$defaultState = [ 'success' => $project -> getAttribute ( 'url' , '' ), 'failure' => '' ];
$validateURL = new URL ();
$appId = $project -> getAttribute ( 'usersOauth' . ucfirst ( $provider ) . 'Appid' , '' );
$appSecret = $project -> getAttribute ( 'usersOauth' . ucfirst ( $provider ) . 'Secret' , '{}' );
$appSecret = json_decode ( $appSecret , true );
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
$key = $request -> getServer ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , hex2bin ( $appSecret [ 'iv' ]), hex2bin ( $appSecret [ 'tag' ]));
}
$classname = 'Auth\\OAuth\\' . ucfirst ( $provider );
if ( ! class_exists ( $classname )) {
throw new Exception ( 'Provider is not supported' , 501 );
}
$oauth = new $classname ( $appId , $appSecret , $callback );
if ( ! empty ( $state )) {
try {
$state = array_merge ( $defaultState , $oauth -> parseState ( $state ));
} catch ( \Exception $exception ) {
throw new Exception ( 'Failed to parse login state params as passed from OAuth provider' );
}
} else {
$state = $defaultState ;
}
if ( ! $validateURL -> isValid ( $state [ 'success' ])) {
throw new Exception ( 'Invalid redirect URL for success login' , 400 );
}
if ( ! empty ( $state [ 'failure' ]) && ! $validateURL -> isValid ( $state [ 'failure' ])) {
throw new Exception ( 'Invalid redirect URL for failure login' , 400 );
}
2020-01-14 04:15:52 +13:00
$state [ 'failure' ] = null ;
2020-01-06 00:29:42 +13:00
$accessToken = $oauth -> getAccessToken ( $code );
if ( empty ( $accessToken )) {
if ( ! empty ( $state [ 'failure' ])) {
$response -> redirect ( $state [ 'failure' ], 301 , 0 );
}
throw new Exception ( 'Failed to obtain access token' );
}
$oauthID = $oauth -> getUserID ( $accessToken );
2020-01-14 04:15:52 +13:00
2020-01-06 00:29:42 +13:00
if ( empty ( $oauthID )) {
if ( ! empty ( $state [ 'failure' ])) {
$response -> redirect ( $state [ 'failure' ], 301 , 0 );
}
throw new Exception ( 'Missing ID from OAuth provider' , 400 );
}
$current = Auth :: tokenVerify ( $user -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret );
if ( $current ) {
$projectDB -> deleteDocument ( $current ); //throw new Exception('User already logged in', 401);
}
$user = ( empty ( $user -> getUid ())) ? $projectDB -> getCollection ([ // Get user by provider id
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'oauth' . ucfirst ( $provider ) . '=' . $oauthID ,
],
]) : $user ;
if ( empty ( $user )) { // No user logged in or with oauth provider ID, create new one or connect with account with same email
$name = $oauth -> getUserName ( $accessToken );
$email = $oauth -> getUserEmail ( $accessToken );
$user = $projectDB -> getCollection ([ // Get user by provider email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
if ( ! $user || empty ( $user -> getUid ())) { // Last option -> create user alone, generate random password
Authorization :: disable ();
$user = $projectDB -> createDocument ([
'$collection' => Database :: SYSTEM_COLLECTION_USERS ,
'$permissions' => [ 'read' => [ '*' ], 'write' => [ 'user:{self}' ]],
'email' => $email ,
2020-02-10 10:37:28 +13:00
'emailVerification' => true ,
2020-01-06 00:29:42 +13:00
'status' => Auth :: USER_STATUS_ACTIVATED , // Email should already be authenticated by OAuth provider
'password' => Auth :: passwordHash ( Auth :: passwordGenerator ()),
'password-update' => time (),
'registration' => time (),
'reset' => false ,
'name' => $name ,
]);
Authorization :: enable ();
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
}
}
2020-01-12 13:20:35 +13:00
// Create session token, verify user account and update OAuth ID and Access Token
2020-01-06 00:29:42 +13:00
$secret = Auth :: tokenGenerator ();
$expiry = time () + Auth :: TOKEN_EXPIRATION_LOGIN_LONG ;
2020-01-12 02:58:02 +13:00
$session = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $user [ '$uid' ]], 'write' => [ 'user:' . $user [ '$uid' ]]],
'type' => Auth :: TOKEN_TYPE_LOGIN ,
'secret' => Auth :: hash ( $secret ), // On way hash encryption to protect DB leak
'expire' => $expiry ,
'userAgent' => $request -> getServer ( 'HTTP_USER_AGENT' , 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2020-01-06 00:29:42 +13:00
$user
-> setAttribute ( 'oauth' . ucfirst ( $provider ), $oauthID )
-> setAttribute ( 'oauth' . ucfirst ( $provider ) . 'AccessToken' , $accessToken )
-> setAttribute ( 'status' , Auth :: USER_STATUS_ACTIVATED )
2020-01-12 02:58:02 +13:00
-> setAttribute ( 'tokens' , $session , Document :: SET_TYPE_APPEND )
2020-01-06 00:29:42 +13:00
;
Authorization :: setRole ( 'user:' . $user -> getUid ());
$user = $projectDB -> updateDocument ( $user -> getArrayCopy ());
if ( false === $user ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
$audit
-> setParam ( 'userId' , $user -> getUid ())
-> setParam ( 'event' , 'account.sessions.create' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
-> setParam ( 'data' , [ 'provider' => $provider ])
;
$response
2020-01-15 09:50:49 +13:00
-> addCookie ( Auth :: $cookieName . '_legacy' , Auth :: encodeSession ( $user -> getUid (), $secret ), $expiry , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
2020-01-15 10:11:02 +13:00
-> addCookie ( Auth :: $cookieName , Auth :: encodeSession ( $user -> getUid (), $secret ), $expiry , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
2020-01-15 09:50:49 +13:00
-> redirect ( $state [ 'success' ])
2020-01-06 00:29:42 +13:00
;
}
);
2020-02-01 11:34:07 +13:00
$utopia -> get ( '/v1/account' )
-> desc ( 'Get Account' )
-> 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' )
-> label ( 'sdk.response' , [ '200' => 'user' ])
-> action (
function () use ( $response , & $user , $oauthKeys ) {
$response -> json ( array_merge ( $user -> getArrayCopy ( array_merge (
[
'$uid' ,
'email' ,
'registration' ,
'name' ,
],
$oauthKeys
)), [ 'roles' => Authorization :: getRoles ()]));
}
);
$utopia -> get ( '/v1/account/prefs' )
-> desc ( 'Get Account Preferences' )
-> 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' )
-> action (
function () use ( $response , $user ) {
$prefs = $user -> getAttribute ( 'prefs' , '{}' );
try {
$prefs = json_decode ( $prefs , true );
$prefs = ( $prefs ) ? $prefs : [];
} catch ( \Exception $error ) {
throw new Exception ( 'Failed to parse prefs' , 500 );
}
$response -> json ( $prefs );
}
);
$utopia -> get ( '/v1/account/sessions' )
-> desc ( 'Get Account Sessions' )
-> 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' )
-> action (
function () use ( $response , $user ) {
$tokens = $user -> getAttribute ( 'tokens' , []);
$reader = new Reader ( __DIR__ . '/../../db/DBIP/dbip-country-lite-2020-01.mmdb' );
$sessions = [];
$current = Auth :: tokenVerify ( $tokens , Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret );
$index = 0 ;
$countries = Locale :: getText ( 'countries' );
foreach ( $tokens as $token ) { /* @var $token Document */
if ( Auth :: TOKEN_TYPE_LOGIN != $token -> getAttribute ( 'type' )) {
continue ;
}
$userAgent = ( ! empty ( $token -> getAttribute ( 'userAgent' ))) ? $token -> getAttribute ( 'userAgent' ) : 'UNKNOWN' ;
$dd = new DeviceDetector ( $userAgent );
// OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
// $dd->skipBotDetection();
$dd -> parse ();
$sessions [ $index ] = [
'$uid' => $token -> getUid (),
'OS' => $dd -> getOs (),
'client' => $dd -> getClient (),
'device' => $dd -> getDevice (),
'brand' => $dd -> getBrand (),
'model' => $dd -> getModel (),
'ip' => $token -> getAttribute ( 'ip' , '' ),
'geo' => [],
'current' => ( $current == $token -> getUid ()) ? true : false ,
];
try {
$record = $reader -> country ( $token -> getAttribute ( 'ip' , '' ));
$sessions [ $index ][ 'geo' ][ 'isoCode' ] = strtolower ( $record -> country -> isoCode );
$sessions [ $index ][ 'geo' ][ 'country' ] = ( isset ( $countries [ $record -> country -> isoCode ])) ? $countries [ $record -> country -> isoCode ] : Locale :: getText ( 'locale.country.unknown' );
} catch ( \Exception $e ) {
$sessions [ $index ][ 'geo' ][ 'isoCode' ] = '--' ;
$sessions [ $index ][ 'geo' ][ 'country' ] = Locale :: getText ( 'locale.country.unknown' );
}
++ $index ;
}
$response -> json ( $sessions );
}
);
$utopia -> get ( '/v1/account/logs' )
-> desc ( 'Get Account Logs' )
-> 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' )
-> action (
function () use ( $response , $register , $project , $user ) {
$adapter = new AuditAdapter ( $register -> get ( 'db' ));
$adapter -> setNamespace ( 'app_' . $project -> getUid ());
$audit = new Audit ( $adapter );
$countries = Locale :: getText ( 'countries' );
$logs = $audit -> getLogsByUserAndActions ( $user -> getUid (), [
'auth.register' , // TODO Deprectate this
'auth.login' , // TODO Deprectate this
'auth.logout' , // TODO Deprectate this
'auth.recovery' , // TODO Deprectate this
'auth.recovery.reset' , // TODO Deprectate this
'auth.oauth.login' , // TODO Deprectate this
'auth.invite' , // TODO Deprectate this
'auth.join' , // TODO Deprectate this
'auth.leave' , // TODO Deprectate this
'account.create' ,
'account.delete' ,
'account.update.name' ,
'account.update.email' ,
'account.update.password' ,
'account.update.prefs' ,
'account.sessions.create' ,
'account.sessions.delete' ,
]);
$reader = new Reader ( __DIR__ . '/../../db/DBIP/dbip-country-lite-2020-01.mmdb' );
$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 ();
$output [ $i ] = [
'event' => $log [ 'event' ],
'ip' => $log [ 'ip' ],
'time' => strtotime ( $log [ 'time' ]),
'OS' => $dd -> getOs (),
'client' => $dd -> getClient (),
'device' => $dd -> getDevice (),
'brand' => $dd -> getBrand (),
'model' => $dd -> getModel (),
'geo' => [],
];
try {
$record = $reader -> country ( $log [ 'ip' ]);
$output [ $i ][ 'geo' ][ 'isoCode' ] = strtolower ( $record -> country -> isoCode );
$output [ $i ][ 'geo' ][ 'country' ] = $record -> country -> name ;
$output [ $i ][ 'geo' ][ 'country' ] = ( isset ( $countries [ $record -> country -> isoCode ])) ? $countries [ $record -> country -> isoCode ] : Locale :: getText ( 'locale.country.unknown' );
} catch ( \Exception $e ) {
$output [ $i ][ 'geo' ][ 'isoCode' ] = '--' ;
$output [ $i ][ 'geo' ][ 'country' ] = Locale :: getText ( 'locale.country.unknown' );
}
}
$response -> json ( $output );
}
);
2019-05-09 18:54:39 +12:00
$utopia -> patch ( '/v1/account/name' )
-> desc ( 'Update Account Name' )
2020-01-05 04:45:28 +13:00
-> label ( 'webhook' , '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-02-10 18:54:26 +13:00
-> param ( 'name' , '' , function () { return new Text ( 100 ); }, 'User name.' )
2019-05-09 18:54:39 +12:00
-> action (
2020-01-12 02:58:02 +13:00
function ( $name ) use ( $response , $user , $projectDB , $audit , $oauthKeys ) {
2019-05-09 18:54:39 +12:00
$user = $projectDB -> updateDocument ( array_merge ( $user -> getArrayCopy (), [
'name' => $name ,
]));
2019-09-07 05:10:41 +12:00
if ( false === $user ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
$audit
-> setParam ( 'event' , 'account.update.name' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
2019-05-09 18:54:39 +12:00
2020-01-12 02:58:02 +13:00
$response -> json ( array_merge ( $user -> getArrayCopy ( array_merge (
[
'$uid' ,
'email' ,
'registration' ,
'name' ,
],
$oauthKeys
)), [ 'roles' => Authorization :: getRoles ()]));
2019-05-09 18:54:39 +12:00
}
);
$utopia -> patch ( '/v1/account/password' )
-> desc ( 'Update Account Password' )
2020-01-05 04:45:28 +13:00
-> label ( 'webhook' , '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-02-10 18:58:29 +13:00
-> param ( 'password' , '' , function () { return new Password (); }, 'New user password.' )
-> param ( 'old-password' , '' , function () { return new Password (); }, 'Old user password.' )
2019-05-09 18:54:39 +12:00
-> action (
2020-01-12 02:58:02 +13:00
function ( $password , $oldPassword ) use ( $response , $user , $projectDB , $audit , $oauthKeys ) {
2019-09-07 05:10:41 +12:00
if ( ! Auth :: passwordVerify ( $oldPassword , $user -> getAttribute ( 'password' ))) { // Double check user password
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid credentials' , 401 );
}
$user = $projectDB -> updateDocument ( array_merge ( $user -> getArrayCopy (), [
'password' => Auth :: passwordHash ( $password ),
]));
2019-09-07 05:10:41 +12:00
if ( false === $user ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
$audit
-> setParam ( 'event' , 'account.update.password' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
2019-05-09 18:54:39 +12:00
2020-01-12 02:58:02 +13:00
$response -> json ( array_merge ( $user -> getArrayCopy ( array_merge (
[
'$uid' ,
'email' ,
'registration' ,
'name' ,
],
$oauthKeys
)), [ 'roles' => Authorization :: getRoles ()]));
2019-05-09 18:54:39 +12:00
}
);
$utopia -> patch ( '/v1/account/email' )
-> desc ( 'Update Account Email' )
2020-01-05 04:45:28 +13:00
-> label ( 'webhook' , '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-02-10 18:58:29 +13:00
-> param ( 'email' , '' , function () { return new Email (); }, 'User email.' )
2020-02-10 18:54:26 +13:00
-> param ( 'password' , '' , function () { return new Password (); }, 'User password.' )
2019-05-09 18:54:39 +12:00
-> action (
2020-01-12 02:58:02 +13:00
function ( $email , $password ) use ( $response , $user , $projectDB , $audit , $oauthKeys ) {
2019-09-07 05:10:41 +12:00
if ( ! Auth :: passwordVerify ( $password , $user -> getAttribute ( 'password' ))) { // Double check user password
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid credentials' , 401 );
}
2019-07-21 23:43:06 +12:00
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
2019-09-07 05:10:41 +12:00
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
2019-07-21 23:43:06 +12:00
]);
2019-09-07 05:10:41 +12:00
if ( ! empty ( $profile )) {
2019-07-21 23:43:06 +12:00
throw new Exception ( 'User already registered' , 400 );
}
2019-05-09 18:54:39 +12:00
// TODO after this user needs to confirm mail again
$user = $projectDB -> updateDocument ( array_merge ( $user -> getArrayCopy (), [
'email' => $email ,
2020-02-10 10:37:28 +13:00
'emailVerification' => false ,
2019-05-09 18:54:39 +12:00
]));
2019-09-07 05:10:41 +12:00
if ( false === $user ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
$audit
-> setParam ( 'event' , 'account.update.email' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
2019-05-09 18:54:39 +12:00
2020-01-12 02:58:02 +13:00
$response -> json ( array_merge ( $user -> getArrayCopy ( array_merge (
[
'$uid' ,
'email' ,
'registration' ,
'name' ,
],
$oauthKeys
)), [ 'roles' => Authorization :: getRoles ()]));
2019-05-09 18:54:39 +12:00
}
);
$utopia -> patch ( '/v1/account/prefs' )
2020-01-06 00:29:42 +13:00
-> desc ( 'Update Account Preferences' )
2020-01-05 04:45:28 +13:00
-> label ( 'webhook' , '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' )
2020-01-19 08:07:02 +13:00
-> param ( 'prefs' , '' , function () { return new Assoc ();}, 'Prefs key-value JSON object.' )
2019-10-08 20:21:54 +13:00
-> label ( 'sdk.description' , '/docs/references/account/update-prefs.md' )
2019-05-09 18:54:39 +12:00
-> action (
2019-09-07 05:10:41 +12:00
function ( $prefs ) use ( $response , $user , $projectDB , $audit ) {
2020-01-19 08:07:02 +13:00
$old = json_decode ( $user -> getAttribute ( 'prefs' , '{}' ), true );
$old = ( $old ) ? $old : [];
2019-05-09 18:54:39 +12:00
$user = $projectDB -> updateDocument ( array_merge ( $user -> getArrayCopy (), [
2020-01-19 08:07:02 +13:00
'prefs' => json_encode ( array_merge ( $old , $prefs )),
2019-05-09 18:54:39 +12:00
]));
2019-09-07 05:10:41 +12:00
if ( false === $user ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-06 00:29:42 +13:00
$audit
-> setParam ( 'event' , 'account.update.prefs' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
2019-05-09 18:54:39 +12:00
2020-01-12 02:58:02 +13:00
$prefs = $user -> getAttribute ( 'prefs' , '{}' );
try {
$prefs = json_decode ( $prefs , true );
2020-01-19 08:07:02 +13:00
$prefs = ( $prefs ) ? $prefs : [];
2020-01-12 02:58:02 +13:00
} catch ( \Exception $error ) {
2020-01-19 08:07:02 +13:00
throw new Exception ( 'Failed to parse prefs' , 500 );
2020-01-12 02:58:02 +13:00
}
$response -> json ( $prefs );
2019-05-09 18:54:39 +12:00
}
);
$utopia -> delete ( '/v1/account' )
-> desc ( 'Delete Account' )
-> label ( 'webhook' , 'account.delete' )
-> 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' )
2019-05-09 18:54:39 +12:00
-> action (
2020-01-06 00:29:42 +13:00
function () use ( $response , $request , $user , $projectDB , $audit , $webhook ) {
2019-05-09 18:54:39 +12:00
$user = $projectDB -> updateDocument ( array_merge ( $user -> getArrayCopy (), [
'status' => Auth :: USER_STATUS_BLOCKED ,
]));
2019-09-07 05:10:41 +12:00
if ( false === $user ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Failed saving user to DB' , 500 );
}
//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
2019-09-07 05:10:41 +12:00
/*
2019-05-09 18:54:39 +12:00
* Data to delete
* * Tokens
* * Memberships
*/
$audit
-> setParam ( 'event' , 'account.delete' )
2020-01-06 00:29:42 +13:00
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
2019-05-09 18:54:39 +12:00
-> setParam ( 'data' , $user -> getArrayCopy ())
;
2020-01-06 00:29:42 +13:00
$webhook
-> setParam ( 'payload' , [
'name' => $user -> getAttribute ( 'name' , '' ),
'email' => $user -> getAttribute ( 'email' , '' ),
])
;
2019-05-09 18:54:39 +12:00
$response
2020-01-15 09:50:49 +13:00
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
2020-01-15 10:11:02 +13:00
-> addCookie ( Auth :: $cookieName , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
2020-01-12 02:58:02 +13:00
-> noContent ()
;
2019-05-09 18:54:39 +12:00
}
2019-09-07 05:10:41 +12:00
);
2020-01-06 00:29:42 +13:00
$utopia -> delete ( '/v1/account/sessions/:id' )
-> desc ( 'Delete Account Session' )
-> label ( 'scope' , 'account' )
-> label ( 'webhook' , '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-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 100 )
-> param ( 'id' , null , function () { return new UID (); }, 'Session unique ID.' )
-> action (
function ( $id ) use ( $response , $request , $user , $projectDB , $webhook , $audit ) {
$tokens = $user -> getAttribute ( 'tokens' , []);
foreach ( $tokens as $token ) { /* @var $token Document */
if (( $id == $token -> getUid ()) && Auth :: TOKEN_TYPE_LOGIN == $token -> getAttribute ( 'type' )) {
if ( ! $projectDB -> deleteDocument ( $token -> getUid ())) {
throw new Exception ( 'Failed to remove token from DB' , 500 );
}
$audit
-> setParam ( 'event' , 'account.sessions.delete' )
-> setParam ( 'resource' , '/user/' . $user -> getUid ())
;
$webhook
-> setParam ( 'payload' , [
'name' => $user -> getAttribute ( 'name' , '' ),
'email' => $user -> getAttribute ( 'email' , '' ),
])
;
if ( $token -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
2020-01-15 09:50:49 +13:00
$response
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
2020-01-15 10:11:02 +13:00
-> addCookie ( Auth :: $cookieName , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
2020-01-15 09:50:49 +13:00
;
2020-01-06 00:29:42 +13:00
}
}
}
2020-01-12 02:58:02 +13:00
$response -> noContent ();
2020-01-06 00:29:42 +13:00
}
);
2020-01-12 02:58:02 +13:00
$utopia -> delete ( '/v1/account/sessions/current' )
-> desc ( 'Delete Current Account Session' )
2020-01-06 00:29:42 +13:00
-> label ( 'webhook' , 'account.sessions.delete' )
2020-01-12 02:58:02 +13:00
-> label ( 'scope' , 'account' )
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' , 'deleteCurrentSession' )
2020-01-12 02:58:02 +13:00
-> label ( 'sdk.description' , '/docs/references/account/delete-session-current.md' )
2020-01-06 00:29:42 +13:00
-> label ( 'abuse-limit' , 100 )
-> action (
function () use ( $response , $request , $user , $projectDB , $audit , $webhook ) {
2020-01-12 02:58:02 +13:00
$token = Auth :: tokenVerify ( $user -> getAttribute ( 'tokens' ), Auth :: TOKEN_TYPE_LOGIN , Auth :: $secret );
2020-01-06 00:29:42 +13:00
2020-01-12 02:58:02 +13:00
if ( ! $projectDB -> deleteDocument ( $token )) {
throw new Exception ( 'Failed to remove token from DB' , 500 );
}
2020-01-06 00:29:42 +13:00
2020-01-12 02:58:02 +13:00
$webhook
-> setParam ( 'payload' , [
'name' => $user -> getAttribute ( 'name' , '' ),
'email' => $user -> getAttribute ( 'email' , '' ),
])
;
2020-01-06 00:29:42 +13:00
2020-01-12 02:58:02 +13:00
$audit -> setParam ( 'event' , 'account.sessions.delete' );
2020-01-06 00:29:42 +13:00
2020-01-12 02:58:02 +13:00
$response
2020-01-15 09:50:49 +13:00
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
2020-01-15 10:11:02 +13:00
-> addCookie ( Auth :: $cookieName , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
2020-01-12 02:58:02 +13:00
-> noContent ()
;
2020-01-06 12:07:41 +13:00
}
);
2020-01-27 19:14:14 +13:00
$utopia -> delete ( '/v1/account/sessions' )
2020-01-24 11:33:44 +13:00
-> desc ( 'Delete All Account Sessions' )
-> label ( 'scope' , 'account' )
-> label ( 'webhook' , 'account.sessions.delete' )
2020-01-27 19:14:14 +13:00
-> label ( 'sdk.platform' , [ APP_PLATFORM_CLIENT ])
2020-01-24 11:33:44 +13:00
-> label ( 'sdk.namespace' , 'account' )
2020-01-31 05:18:46 +13:00
-> label ( 'sdk.method' , 'deleteSessions' )
2020-01-24 11:33:44 +13:00
-> label ( 'sdk.description' , '/docs/references/account/delete-sessions.md' )
-> label ( 'abuse-limit' , 100 )
-> action (
function () use ( $response , $request , $user , $projectDB , $audit , $webhook ) {
$tokens = $user -> getAttribute ( 'tokens' , []);
foreach ( $tokens as $token ) { /* @var $token Document */
if ( ! $projectDB -> deleteDocument ( $token -> getUid ())) {
throw new Exception ( 'Failed to remove token from DB' , 500 );
}
$audit
-> setParam ( 'event' , 'account.sessions.delete' )
-> setParam ( 'resource' , '/user/' . $user -> getUid ())
;
$webhook
-> setParam ( 'payload' , [
'name' => $user -> getAttribute ( 'name' , '' ),
'email' => $user -> getAttribute ( 'email' , '' ),
])
;
if ( $token -> getAttribute ( 'secret' ) == Auth :: hash ( Auth :: $secret )) { // If current session delete the cookies too
$response
-> addCookie ( Auth :: $cookieName . '_legacy' , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , null )
-> addCookie ( Auth :: $cookieName , '' , time () - 3600 , '/' , COOKIE_DOMAIN , ( 'https' == $request -> getServer ( 'REQUEST_SCHEME' , 'https' )), true , COOKIE_SAMESITE )
;
}
}
$response -> noContent ();
}
);
2020-01-06 12:07:41 +13:00
$utopia -> post ( '/v1/account/recovery' )
2020-02-09 07:51:49 +13:00
-> desc ( 'Create Password Recovery' )
2020-01-06 12:07:41 +13:00
-> label ( 'scope' , 'public' )
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' )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-02-10 18:58:29 +13:00
-> param ( 'email' , '' , function () { return new Email (); }, 'User email.' )
2020-01-12 13:20:35 +13:00
-> param ( 'url' , '' , function () use ( $clients ) { return new Host ( $clients ); }, 'URL to redirect the user back to your app from the recovery email.' )
2020-01-06 12:07:41 +13:00
-> action (
2020-01-12 13:20:35 +13:00
function ( $email , $url ) use ( $request , $response , $projectDB , $register , $audit , $project ) {
2020-01-06 12:07:41 +13:00
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'email=' . $email ,
],
]);
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
$secret = Auth :: tokenGenerator ();
2020-01-12 02:58:02 +13:00
$recovery = new Document ([
2020-01-06 12:07:41 +13:00
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $profile -> getUid ()], 'write' => [ 'user:' . $profile -> getUid ()]],
'type' => Auth :: TOKEN_TYPE_RECOVERY ,
'secret' => Auth :: hash ( $secret ), // On way hash encryption to protect DB leak
'expire' => time () + Auth :: TOKEN_EXPIRATION_RECOVERY ,
'userAgent' => $request -> getServer ( 'HTTP_USER_AGENT' , 'UNKNOWN' ),
'ip' => $request -> getIP (),
2020-01-12 02:58:02 +13:00
]);
2020-01-06 12:07:41 +13:00
Authorization :: setRole ( 'user:' . $profile -> getUid ());
2020-01-12 02:58:02 +13:00
$recovery = $projectDB -> createDocument ( $recovery -> getArrayCopy ());
if ( false === $recovery ) {
throw new Exception ( 'Failed saving recovery to DB' , 500 );
}
$profile -> setAttribute ( 'tokens' , $recovery , Document :: SET_TYPE_APPEND );
2020-01-06 12:07:41 +13:00
$profile = $projectDB -> updateDocument ( $profile -> getArrayCopy ());
if ( false === $profile ) {
throw new Exception ( 'Failed to save user to DB' , 500 );
}
2020-01-12 13:20:35 +13:00
$url = Template :: parseURL ( $url );
2020-01-20 01:22:45 +13:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $profile -> getUid (), 'secret' => $secret ]);
2020-01-12 13:20:35 +13:00
$url = Template :: unParseURL ( $url );
2020-01-06 12:07:41 +13:00
2020-02-10 20:34:02 +13:00
$body = new Template ( __DIR__ . '/../../config/locales/templates/' . Locale :: getText ( 'account.emails.recovery.body' ));
2020-01-06 12:07:41 +13:00
$body
-> setParam ( '{{direction}}' , Locale :: getText ( 'settings.direction' ))
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' , [ '[APP-NAME]' ]))
-> setParam ( '{{name}}' , $profile -> getAttribute ( 'name' ))
2020-01-12 13:20:35 +13:00
-> setParam ( '{{redirect}}' , $url )
2020-01-06 12:07:41 +13:00
;
$mail = $register -> get ( 'smtp' ); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */
$mail -> addAddress ( $profile -> getAttribute ( 'email' , '' ), $profile -> getAttribute ( 'name' , '' ));
2020-02-10 20:34:02 +13:00
$mail -> Subject = Locale :: getText ( 'account.emails.recovery.title' );
2020-01-06 12:07:41 +13:00
$mail -> Body = $body -> render ();
$mail -> AltBody = strip_tags ( $body -> render ());
try {
$mail -> send ();
} catch ( \Exception $error ) {
2020-01-12 02:58:02 +13:00
throw new Exception ( 'Error sending mail: ' . $error -> getMessage (), 500 );
2020-01-06 12:07:41 +13:00
}
$audit
-> setParam ( 'userId' , $profile -> getUid ())
-> setParam ( 'event' , 'account.recovery.create' )
2020-01-12 13:20:35 +13:00
-> setParam ( 'resource' , 'users/' . $profile -> getUid ())
2020-01-06 12:07:41 +13:00
;
2020-01-12 02:58:02 +13:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> json ( $recovery -> getArrayCopy ([ '$uid' , 'type' , 'expire' ]))
;
2020-01-06 12:07:41 +13:00
}
);
$utopia -> put ( '/v1/account/recovery' )
2020-02-09 07:51:49 +13:00
-> desc ( 'Complete Password Recovery' )
2020-01-06 12:07:41 +13:00
-> label ( 'scope' , 'public' )
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' )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2020-01-12 02:58:02 +13:00
-> param ( 'userId' , '' , function () { return new UID (); }, 'User account UID address.' )
2020-01-20 01:22:45 +13:00
-> param ( 'secret' , '' , function () { return new Text ( 256 ); }, 'Valid reset token.' )
2020-01-06 12:07:41 +13:00
-> param ( 'password-a' , '' , function () { return new Password (); }, 'New password.' )
-> param ( 'password-b' , '' , function () { return new Password (); }, 'New password again.' )
-> action (
2020-01-20 01:22:45 +13:00
function ( $userId , $secret , $passwordA , $passwordB ) use ( $response , $projectDB , $audit ) {
2020-01-06 12:07:41 +13:00
if ( $passwordA !== $passwordB ) {
throw new Exception ( 'Passwords must match' , 400 );
}
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'$uid=' . $userId ,
],
]);
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
2020-01-20 01:22:45 +13:00
$recovery = Auth :: tokenVerify ( $profile -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_RECOVERY , $secret );
2020-01-06 12:07:41 +13:00
2020-01-12 02:58:02 +13:00
if ( ! $recovery ) {
throw new Exception ( 'Invalid recovery token' , 401 );
2020-01-06 12:07:41 +13:00
}
Authorization :: setRole ( 'user:' . $profile -> getUid ());
$profile = $projectDB -> updateDocument ( array_merge ( $profile -> getArrayCopy (), [
'password' => Auth :: passwordHash ( $passwordA ),
'password-update' => time (),
2020-02-10 10:37:28 +13:00
'emailVerification' => true ,
2020-01-06 12:07:41 +13:00
]));
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
2020-01-12 02:58:02 +13: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
}
$audit
-> setParam ( 'userId' , $profile -> getUid ())
-> setParam ( 'event' , 'account.recovery.update' )
2020-01-12 13:20:35 +13:00
-> setParam ( 'resource' , 'users/' . $profile -> getUid ())
2020-01-06 12:07:41 +13:00
;
2020-01-12 02:58:02 +13:00
$recovery = $profile -> search ( '$uid' , $recovery , $profile -> getAttribute ( 'tokens' , []));
$response -> json ( $recovery -> getArrayCopy ([ '$uid' , 'type' , 'expire' ]));
2020-01-06 00:29:42 +13:00
}
2020-01-12 13:20:35 +13:00
);
2020-02-10 20:34:02 +13:00
$utopia -> post ( '/v1/account/verification' )
2020-02-10 10:37:28 +13:00
-> desc ( 'Create Email Verification' )
2020-01-12 13:20:35 +13:00
-> label ( 'scope' , 'account' )
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' )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2020-02-10 10:37:28 +13:00
-> param ( 'url' , '' , function () use ( $clients ) { return new Host ( $clients ); }, 'URL to redirect the user back to your app from the verification email.' ) // TODO add built-in confirm page
2020-01-12 13:20:35 +13:00
-> action (
function ( $url ) use ( $request , $response , $register , $user , $project , $projectDB , $audit ) {
$verificationSecret = Auth :: tokenGenerator ();
$verification = new Document ([
'$collection' => Database :: SYSTEM_COLLECTION_TOKENS ,
'$permissions' => [ 'read' => [ 'user:' . $user -> getUid ()], 'write' => [ 'user:' . $user -> getUid ()]],
'type' => Auth :: TOKEN_TYPE_VERIFICATION ,
'secret' => Auth :: hash ( $verificationSecret ), // On way hash encryption to protect DB leak
'expire' => time () + Auth :: TOKEN_EXPIRATION_CONFIRM ,
'userAgent' => $request -> getServer ( 'HTTP_USER_AGENT' , 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
Authorization :: setRole ( 'user:' . $user -> getUid ());
$verification = $projectDB -> createDocument ( $verification -> getArrayCopy ());
if ( false === $verification ) {
throw new Exception ( 'Failed saving verification to DB' , 500 );
}
$user -> setAttribute ( 'tokens' , $verification , Document :: SET_TYPE_APPEND );
$user = $projectDB -> updateDocument ( $user -> getArrayCopy ());
if ( false === $user ) {
throw new Exception ( 'Failed to save user to DB' , 500 );
}
$url = Template :: parseURL ( $url );
2020-01-20 01:22:45 +13:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getUid (), 'secret' => $verificationSecret ]);
2020-01-12 13:20:35 +13:00
$url = Template :: unParseURL ( $url );
2020-02-10 20:34:02 +13:00
$body = new Template ( __DIR__ . '/../../config/locales/templates/' . Locale :: getText ( 'account.emails.verification.body' ));
2020-01-12 13:20:35 +13:00
$body
-> setParam ( '{{direction}}' , Locale :: getText ( 'settings.direction' ))
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' , [ '[APP-NAME]' ]))
-> setParam ( '{{name}}' , $user -> getAttribute ( 'name' ))
-> setParam ( '{{redirect}}' , $url )
;
$mail = $register -> get ( 'smtp' ); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */
$mail -> addAddress ( $user -> getAttribute ( 'email' ), $user -> getAttribute ( 'name' ));
2020-02-10 20:34:02 +13:00
$mail -> Subject = Locale :: getText ( 'account.emails.verification.title' );
2020-01-12 13:20:35 +13:00
$mail -> Body = $body -> render ();
$mail -> AltBody = strip_tags ( $body -> render ());
try {
$mail -> send ();
} catch ( \Exception $error ) {
throw new Exception ( 'Problem sending mail: ' . $error -> getMessage (), 500 );
}
$audit
-> setParam ( 'userId' , $user -> getUid ())
-> setParam ( 'event' , 'account.verification.create' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> json ( $verification -> getArrayCopy ([ '$uid' , 'type' , 'expire' ]))
;
}
);
$utopia -> put ( '/v1/account/verification' )
2020-02-10 10:37:28 +13:00
-> desc ( 'Complete Email Verification' )
2020-01-12 13:20:35 +13:00
-> label ( 'scope' , 'public' )
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' )
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2020-02-10 18:54:26 +13:00
-> param ( 'userId' , '' , function () { return new UID (); }, 'User unique ID.' )
2020-02-10 20:34:02 +13:00
-> param ( 'secret' , '' , function () { return new Text ( 256 ); }, 'Valid verification token.' )
2020-01-12 13:20:35 +13:00
-> action (
2020-01-20 01:22:45 +13:00
function ( $userId , $secret ) use ( $response , $user , $projectDB , $audit ) {
2020-01-12 13:20:35 +13:00
$profile = $projectDB -> getCollection ([ // Get user by email address
'limit' => 1 ,
'first' => true ,
'filters' => [
'$collection=' . Database :: SYSTEM_COLLECTION_USERS ,
'$uid=' . $userId ,
],
]);
if ( empty ( $profile )) {
throw new Exception ( 'User not found' , 404 ); // TODO maybe hide this
}
2020-01-20 01:22:45 +13:00
$verification = Auth :: tokenVerify ( $profile -> getAttribute ( 'tokens' , []), Auth :: TOKEN_TYPE_VERIFICATION , $secret );
2020-01-12 13:20:35 +13:00
if ( ! $verification ) {
throw new Exception ( 'Invalid verification token' , 401 );
}
Authorization :: setRole ( 'user:' . $profile -> getUid ());
$profile = $projectDB -> updateDocument ( array_merge ( $profile -> getArrayCopy (), [
2020-02-10 10:37:28 +13:00
'emailVerification' => true ,
2020-01-12 13:20:35 +13:00
]));
if ( false === $profile ) {
throw new Exception ( 'Failed saving user to DB' , 500 );
}
/**
* 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 );
}
$audit
-> setParam ( 'userId' , $profile -> getUid ())
-> setParam ( 'event' , 'account.verification.update' )
-> setParam ( 'resource' , 'users/' . $user -> getUid ())
;
$verification = $profile -> search ( '$uid' , $verification , $profile -> getAttribute ( 'tokens' , []));
$response -> json ( $verification -> getArrayCopy ([ '$uid' , 'type' , 'expire' ]));
}
2020-01-06 00:29:42 +13:00
);