2019-05-09 06:54:39 +00:00
< ? php
2021-06-12 19:37:19 +00:00
use Ahc\Jwt\JWT ;
2024-03-01 02:07:58 +00:00
use Appwrite\Auth\MFA\Type ;
2023-07-15 00:28:45 +00:00
use Appwrite\Auth\OAuth2\Exception as OAuth2Exception ;
2024-03-06 17:34:21 +00:00
use Appwrite\Auth\Phrase ;
2020-03-24 17:56:32 +00:00
use Appwrite\Auth\Validator\Password ;
2024-03-06 17:34:21 +00:00
use Appwrite\Auth\Validator\PasswordDictionary ;
use Appwrite\Auth\Validator\PasswordHistory ;
use Appwrite\Auth\Validator\PersonalData ;
2022-08-14 15:10:12 +00:00
use Appwrite\Auth\Validator\Phone ;
2021-02-14 17:28:54 +00:00
use Appwrite\Detector\Detector ;
2024-03-06 17:34:21 +00:00
use Appwrite\Event\Delete ;
2022-05-18 16:14:21 +00:00
use Appwrite\Event\Event ;
use Appwrite\Event\Mail ;
2024-03-06 17:34:21 +00:00
use Appwrite\Event\Messaging ;
2025-01-30 04:53:53 +00:00
use Appwrite\Event\StatsUsage ;
2022-08-11 23:53:52 +00:00
use Appwrite\Extend\Exception ;
2024-03-06 17:34:21 +00:00
use Appwrite\Hooks\Hooks ;
2025-11-11 13:25:10 +00:00
use Appwrite\Network\Validator\Email as EmailValidator ;
2025-04-14 11:56:42 +00:00
use Appwrite\Network\Validator\Redirect ;
2021-08-05 05:06:38 +00:00
use Appwrite\OpenSSL\OpenSSL ;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\AuthType ;
use Appwrite\SDK\ContentType ;
2025-08-20 14:08:27 +00:00
use Appwrite\SDK\Deprecated ;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\Method ;
use Appwrite\SDK\MethodType ;
use Appwrite\SDK\Response as SDKResponse ;
2021-08-05 05:06:38 +00:00
use Appwrite\Template\Template ;
use Appwrite\URL\URL as URLParser ;
2025-11-04 03:48:57 +00:00
use Appwrite\Utopia\Database\Documents\User ;
2022-08-11 23:53:52 +00:00
use Appwrite\Utopia\Database\Validator\CustomId ;
2023-05-18 01:11:45 +00:00
use Appwrite\Utopia\Database\Validator\Queries\Identities ;
2022-05-18 16:14:21 +00:00
use Appwrite\Utopia\Request ;
2021-08-05 05:06:38 +00:00
use Appwrite\Utopia\Response ;
2026-01-15 13:19:34 +00:00
use libphonenumber\NumberParseException ;
2025-01-08 11:31:01 +00:00
use libphonenumber\PhoneNumberUtil ;
2022-05-18 16:14:21 +00:00
use MaxMind\Db\Reader ;
2025-12-14 07:58:16 +00:00
use Utopia\Audit\Audit ;
2025-11-04 03:48:57 +00:00
use Utopia\Auth\Hashes\Sha ;
use Utopia\Auth\Proofs\Code as ProofsCode ;
use Utopia\Auth\Proofs\Password as ProofsPassword ;
use Utopia\Auth\Proofs\Token as ProofsToken ;
use Utopia\Auth\Store ;
2021-08-05 05:06:38 +00:00
use Utopia\Config\Config ;
2022-05-18 16:14:21 +00:00
use Utopia\Database\Database ;
2022-07-12 13:32:39 +00:00
use Utopia\Database\DateTime ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Document ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Exception\Duplicate ;
2025-04-16 11:59:36 +00:00
use Utopia\Database\Exception\Order as OrderException ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Exception\Query as QueryException ;
2023-05-29 13:58:45 +00:00
use Utopia\Database\Helpers\ID ;
use Utopia\Database\Helpers\Permission ;
use Utopia\Database\Helpers\Role ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Query ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Validator\Authorization ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Queries ;
2024-10-17 05:41:24 +00:00
use Utopia\Database\Validator\Query\Cursor ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Query\Limit ;
use Utopia\Database\Validator\Query\Offset ;
2021-05-06 22:31:05 +00:00
use Utopia\Database\Validator\UID ;
2025-11-11 13:25:10 +00:00
use Utopia\Emails\Email ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Http ;
2022-05-18 16:14:21 +00:00
use Utopia\Locale\Locale ;
2025-10-07 10:37:47 +00:00
use Utopia\Storage\Validator\FileName ;
2024-04-01 11:08:46 +00:00
use Utopia\System\System ;
2025-12-07 20:29:45 +00:00
use Utopia\Validator ;
2021-08-05 05:06:38 +00:00
use Utopia\Validator\ArrayList ;
use Utopia\Validator\Assoc ;
2024-03-06 17:34:21 +00:00
use Utopia\Validator\Boolean ;
2025-12-22 17:08:48 +00:00
use Utopia\Validator\Range ;
2021-08-05 05:06:38 +00:00
use Utopia\Validator\Text ;
use Utopia\Validator\WhiteList ;
2019-05-09 06:54:39 +00:00
2024-08-12 19:59:42 +00:00
$oauthDefaultSuccess = '/console/auth/oauth2/success' ;
$oauthDefaultFailure = '/console/auth/oauth2/failure' ;
2020-04-08 13:38:36 +00:00
2025-12-17 08:56:45 +00:00
function sendSessionAlert ( Locale $locale , Document $user , Document $project , array $platform , Document $session , Mail $queueForMails )
2024-06-24 13:12:09 +00:00
{
$subject = $locale -> getText ( " emails.sessionAlert.subject " );
2025-07-23 16:34:25 +00:00
$preview = $locale -> getText ( " emails.sessionAlert.preview " );
2024-06-24 13:12:09 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.sessionAlert-' . $locale -> default ] ? ? [];
2025-10-31 12:22:47 +00:00
$smtpBaseTemplate = $project -> getAttribute ( 'smtpBaseTemplate' , 'email-base' );
$validator = new FileName ();
if ( ! $validator -> isValid ( $smtpBaseTemplate )) {
throw new Exception ( Exception :: GENERAL_BAD_REQUEST , 'Invalid template path' );
}
$bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl' ;
2024-06-24 13:12:09 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-session-alert.tpl' );
$message
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.sessionAlert.hello " ))
-> setParam ( '{{body}}' , $locale -> getText ( " emails.sessionAlert.body " ))
2024-06-26 08:42:01 +00:00
-> setParam ( '{{listDevice}}' , $locale -> getText ( " emails.sessionAlert.listDevice " ))
-> setParam ( '{{listIpAddress}}' , $locale -> getText ( " emails.sessionAlert.listIpAddress " ))
-> setParam ( '{{listCountry}}' , $locale -> getText ( " emails.sessionAlert.listCountry " ))
2024-06-24 13:12:09 +00:00
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.sessionAlert.footer " ))
2024-06-26 14:46:12 +00:00
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.sessionAlert.thanks " ))
2024-06-24 13:12:09 +00:00
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.sessionAlert.signature " ));
$body = $message -> render ();
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
$replyTo = " " ;
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
}
$queueForMails
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
}
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
}
$queueForMails
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
}
2025-07-20 11:58:44 +00:00
// session alerts should always have a client name!
$clientName = $session -> getAttribute ( 'clientName' );
if ( empty ( $clientName )) {
2025-07-20 11:10:29 +00:00
// fallback to the user agent and then unknown!
2025-07-20 11:58:44 +00:00
$userAgent = $session -> getAttribute ( 'userAgent' );
$clientName = ! empty ( $userAgent ) ? $userAgent : 'UNKNOWN' ;
$session -> setAttribute ( 'clientName' , $clientName );
2025-07-20 11:10:29 +00:00
}
2025-12-17 09:03:49 +00:00
$projectName = $project -> getAttribute ( 'name' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2024-06-24 13:12:09 +00:00
$emailVariables = [
'direction' => $locale -> getText ( 'settings.direction' ),
2024-09-05 14:33:16 +00:00
'date' => ( new \DateTime ()) -> format ( 'F j' ),
'year' => ( new \DateTime ()) -> format ( 'YYYY' ),
'time' => ( new \DateTime ()) -> format ( 'H:i:s' ),
2024-06-24 13:12:09 +00:00
'user' => $user -> getAttribute ( 'name' ),
2025-12-17 09:03:49 +00:00
'project' => $projectName ,
2024-06-26 14:46:12 +00:00
'device' => $session -> getAttribute ( 'clientName' ),
'ipAddress' => $session -> getAttribute ( 'ip' ),
'country' => $locale -> getText ( 'countries.' . $session -> getAttribute ( 'countryCode' ), $locale -> getText ( 'locale.country.unknown' )),
2024-06-24 13:12:09 +00:00
];
2025-10-31 12:22:47 +00:00
if ( $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ) {
$emailVariables = array_merge ( $emailVariables , [
2025-12-17 09:16:24 +00:00
'accentColor' => $platform [ 'accentColor' ],
2025-12-17 08:56:45 +00:00
'logoUrl' => $platform [ 'logoUrl' ],
'twitter' => $platform [ 'twitterUrl' ],
'discord' => $platform [ 'discordUrl' ],
'github' => $platform [ 'githubUrl' ],
'terms' => $platform [ 'termsUrl' ],
'privacy' => $platform [ 'privacyUrl' ],
2025-12-17 09:03:49 +00:00
'platform' => $platform [ 'platformName' ],
2025-10-31 12:22:47 +00:00
]);
}
2024-06-24 13:12:09 +00:00
$email = $user -> getAttribute ( 'email' );
$queueForMails
-> setSubject ( $subject )
2025-07-23 16:34:25 +00:00
-> setPreview ( $preview )
2024-06-24 13:12:09 +00:00
-> setBody ( $body )
2025-10-31 12:22:47 +00:00
-> setBodyTemplate ( $bodyTemplate )
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2025-12-17 10:13:10 +00:00
-> setRecipient ( $email );
// since this is console project, set email sender name!
if ( $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ) {
$queueForMails -> setSenderName ( $platform [ 'emailSenderName' ]);
}
$queueForMails -> trigger ();
2025-09-18 07:03:42 +00:00
}
2024-06-24 13:12:09 +00:00
2025-12-17 08:56:45 +00:00
2026-01-14 15:08:00 +00:00
$createSession = function ( string $userId , string $secret , Request $request , Response $response , User $user , Database $dbForProject , Document $project , array $platform , Locale $locale , Reader $geodb , Event $queueForEvents , Mail $queueForMails , Store $store , ProofsToken $proofForToken , ProofsCode $proofForCode , Authorization $authorization ) {
2025-10-21 01:59:30 +00:00
2025-11-04 03:48:57 +00:00
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
2026-01-14 15:08:00 +00:00
$userFromRequest = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2024-03-06 18:07:58 +00:00
if ( $userFromRequest -> isEmpty ()) {
throw new Exception ( Exception :: USER_INVALID_TOKEN );
}
2025-11-04 03:48:57 +00:00
$verifiedToken = $userFromRequest -> tokenVerify ( null , $secret , $proofForToken )
? : $userFromRequest -> tokenVerify ( null , $secret , $proofForCode );
2024-03-06 18:07:58 +00:00
if ( ! $verifiedToken ) {
throw new Exception ( Exception :: USER_INVALID_TOKEN );
}
$user -> setAttributes ( $userFromRequest -> getArrayCopy ());
2025-11-04 03:48:57 +00:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2024-03-06 18:07:58 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2025-11-04 03:48:57 +00:00
$sessionSecret = $proofForToken -> generate ();
2024-03-06 18:07:58 +00:00
$factor = ( match ( $verifiedToken -> getAttribute ( 'type' )) {
2025-11-04 03:48:57 +00:00
TOKEN_TYPE_MAGIC_URL ,
TOKEN_TYPE_OAUTH2 ,
TOKEN_TYPE_EMAIL => Type :: EMAIL ,
TOKEN_TYPE_PHONE => Type :: PHONE ,
TOKEN_TYPE_GENERIC => 'token' ,
2024-03-06 18:07:58 +00:00
default => throw new Exception ( Exception :: USER_INVALID_TOKEN )
});
2025-11-05 07:09:18 +00:00
$provider = match ( $verifiedToken -> getAttribute ( 'type' )) {
TOKEN_TYPE_VERIFICATION ,
TOKEN_TYPE_RECOVERY ,
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL ,
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL ,
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE ,
TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2 ,
default => SESSION_PROVIDER_TOKEN ,
};
2024-03-06 18:07:58 +00:00
$session = new Document ( array_merge (
[
'$id' => ID :: unique (),
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-05 07:09:18 +00:00
'provider' => $provider ,
2025-11-04 03:48:57 +00:00
'secret' => $proofForToken -> hash ( $sessionSecret ), // One way hash encryption to protect DB leak
2024-03-06 18:07:58 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'factors' => [ $factor ],
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2025-09-11 08:46:38 +00:00
'expire' => DateTime :: addSeconds ( new \DateTime (), $duration )
2024-03-06 18:07:58 +00:00
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
));
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2024-03-06 18:07:58 +00:00
$session = $dbForProject -> createDocument ( 'sessions' , $session
-> setAttribute ( '$permissions' , [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
]));
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> deleteDocument ( 'tokens' , $verifiedToken -> getId ()));
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2024-05-07 09:01:57 +00:00
// Magic URL + Email OTP
2025-11-04 03:48:57 +00:00
if ( $verifiedToken -> getAttribute ( 'type' ) === TOKEN_TYPE_MAGIC_URL || $verifiedToken -> getAttribute ( 'type' ) === TOKEN_TYPE_EMAIL ) {
2024-03-06 18:07:58 +00:00
$user -> setAttribute ( 'emailVerification' , true );
}
2025-11-04 03:48:57 +00:00
if ( $verifiedToken -> getAttribute ( 'type' ) === TOKEN_TYPE_PHONE ) {
2024-03-06 18:07:58 +00:00
$user -> setAttribute ( 'phoneVerification' , true );
}
try {
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
} catch ( \Throwable $th ) {
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed saving user to DB' );
}
2024-08-17 11:01:10 +00:00
$isAllowedTokenType = match ( $verifiedToken -> getAttribute ( 'type' )) {
2025-11-04 03:48:57 +00:00
TOKEN_TYPE_MAGIC_URL ,
TOKEN_TYPE_EMAIL => false ,
2024-08-17 11:01:10 +00:00
default => true
};
$hasUserEmail = $user -> getAttribute ( 'email' , false ) !== false ;
$isSessionAlertsEnabled = $project -> getAttribute ( 'auths' , [])[ 'sessionAlerts' ] ? ? false ;
$isNotFirstSession = $dbForProject -> count ( 'sessions' , [
Query :: equal ( 'userId' , [ $user -> getId ()]),
]) !== 1 ;
if ( $isAllowedTokenType && $hasUserEmail && $isSessionAlertsEnabled && $isNotFirstSession ) {
2025-12-17 08:56:45 +00:00
sendSessionAlert ( $locale , $user , $project , $platform , $session , $queueForMails );
2024-06-24 13:12:09 +00:00
}
2024-03-06 18:07:58 +00:00
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ());
2025-11-04 03:48:57 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $sessionSecret )
-> encode ();
2024-03-06 18:07:58 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2025-11-04 03:48:57 +00:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ $store -> getKey () => $encoded ]));
2024-03-06 18:07:58 +00:00
}
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
$protocol = $request -> getProtocol ();
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2024-03-06 18:07:58 +00:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED );
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , $countryName )
-> setAttribute ( 'expire' , $expire )
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'secret' , $encoded )
2024-03-06 18:07:58 +00:00
;
$response -> dynamic ( $session , Response :: MODEL_SESSION );
};
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account' )
2023-10-03 16:50:48 +00:00
-> desc ( 'Create account' )
2021-02-28 18:36:13 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2025-01-10 03:12:10 +00:00
-> label ( 'auth.type' , 'email-password' )
2022-09-08 13:06:16 +00:00
-> label ( 'audits.event' , 'user.create' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-08-16 14:56:05 +00:00
-> label ( 'audits.userId' , '{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'create' ,
description : '/docs/references/account/create.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2020-01-03 21:00:53 +00:00
-> label ( 'abuse-limit' , 10 )
2024-01-12 17:26:01 +00:00
-> param ( 'userId' , '' , new CustomId (), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2024-01-02 10:59:35 +00:00
-> param ( 'password' , '' , fn ( $project , $passwordsDictionary ) => new PasswordDictionary ( $passwordsDictionary , $project -> getAttribute ( 'auths' , [])[ 'passwordDictionary' ] ? ? false ), 'New user password. Must be between 8 and 256 chars.' , false , [ 'project' , 'passwordsDictionary' ])
2020-09-10 14:40:14 +00:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' , true )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2020-12-26 14:31:53 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2023-12-15 22:19:43 +00:00
-> inject ( 'hooks' )
2026-01-14 15:08:00 +00:00
-> action ( function ( string $userId , string $email , string $password , string $name , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Authorization $authorization , Hooks $hooks ) {
2020-06-29 21:43:34 +00:00
2021-06-03 12:58:40 +00:00
$email = \strtolower ( $email );
2020-06-29 21:43:34 +00:00
if ( 'console' === $project -> getId ()) {
2021-06-03 12:58:40 +00:00
$whitelistEmails = $project -> getAttribute ( 'authWhitelistEmails' );
$whitelistIPs = $project -> getAttribute ( 'authWhitelistIPs' );
2020-06-29 21:43:34 +00:00
2023-05-29 13:58:45 +00:00
if ( ! empty ( $whitelistEmails ) && ! \in_array ( $email , $whitelistEmails ) && ! \in_array ( strtoupper ( $email ), $whitelistEmails )) {
2022-07-22 14:38:06 +00:00
throw new Exception ( Exception :: USER_EMAIL_NOT_WHITELISTED );
2020-01-03 21:00:53 +00:00
}
2021-06-03 12:58:40 +00:00
if ( ! empty ( $whitelistIPs ) && ! \in_array ( $request -> getIP (), $whitelistIPs )) {
2022-07-22 14:38:06 +00:00
throw new Exception ( Exception :: USER_IP_NOT_WHITELISTED );
2020-01-03 21:00:53 +00:00
}
2020-06-29 21:43:34 +00:00
}
2020-01-03 21:00:53 +00:00
2021-08-06 08:34:17 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-02-28 18:36:13 +00:00
if ( $limit !== 0 ) {
2022-05-16 09:58:17 +00:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-02-28 18:36:13 +00:00
2022-02-27 09:57:09 +00:00
if ( $total >= $limit ) {
2024-04-18 21:22:41 +00:00
if ( 'console' === $project -> getId ()) {
2024-04-18 21:08:47 +00:00
throw new Exception ( Exception :: USER_CONSOLE_COUNT_EXCEEDED );
}
2022-07-22 14:38:06 +00:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-02-28 18:36:13 +00:00
}
}
2023-05-18 01:11:45 +00:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2024-10-31 08:13:23 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2024-03-01 07:37:31 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST ); /** Return a generic bad request to prevent exposing existing accounts */
2023-05-18 01:11:45 +00:00
}
2023-07-19 22:24:32 +00:00
if ( $project -> getAttribute ( 'auths' , [])[ 'personalDataCheck' ] ? ? false ) {
2023-04-13 20:20:03 +00:00
$personalDataValidator = new PersonalData ( $userId , $email , $name , null );
if ( ! $personalDataValidator -> isValid ( $password )) {
throw new Exception ( Exception :: USER_PASSWORD_PERSONAL_DATA );
}
}
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , true ]);
2023-12-15 22:19:43 +00:00
2022-12-18 06:31:14 +00:00
$passwordHistory = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
2025-11-04 03:48:57 +00:00
$proof = new ProofsPassword ();
$hash = $proof -> hash ( $password );
2025-11-02 08:59:28 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-02 08:59:28 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-02 08:59:28 +00:00
}
2020-06-29 21:43:34 +00:00
try {
2022-08-14 14:22:38 +00:00
$userId = $userId == 'unique()' ? ID :: unique () : $userId ;
2023-07-07 00:12:39 +00:00
$user -> setAttributes ([
2022-08-15 11:24:31 +00:00
'$id' => $userId ,
2022-08-02 09:21:53 +00:00
'$permissions' => [
2022-08-14 05:21:11 +00:00
Permission :: read ( Role :: any ()),
2022-08-15 11:24:31 +00:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 09:21:53 +00:00
],
2020-06-29 21:43:34 +00:00
'email' => $email ,
'emailVerification' => false ,
2021-07-14 11:02:12 +00:00
'status' => true ,
2025-11-04 03:48:57 +00:00
'password' => $hash ,
'passwordHistory' => $passwordHistory > 0 ? [ $hash ] : [],
2023-02-20 01:51:56 +00:00
'passwordUpdate' => DateTime :: now (),
2025-11-04 03:48:57 +00:00
'hash' => $proof -> getHash () -> getName (),
'hashOptions' => $proof -> getHash () -> getOptions (),
2022-07-13 14:02:49 +00:00
'registration' => DateTime :: now (),
2020-06-29 21:43:34 +00:00
'reset' => false ,
'name' => $name ,
2024-01-10 16:22:32 +00:00
'mfa' => false ,
2021-12-28 10:48:50 +00:00
'prefs' => new \stdClass (),
2022-04-26 10:36:49 +00:00
'sessions' => null ,
2022-04-27 11:06:53 +00:00
'tokens' => null ,
2022-04-27 12:44:47 +00:00
'memberships' => null ,
2024-02-29 20:59:49 +00:00
'authenticators' => null ,
2023-07-07 00:12:39 +00:00
'search' => implode ( ' ' , [ $userId , $email , $name ]),
2023-08-23 01:34:23 +00:00
'accessedAt' => DateTime :: now (),
2025-11-11 13:25:10 +00:00
'emailCanonical' => $emailCanonical ? -> getCanonical (),
'emailIsCanonical' => $emailCanonical ? -> isCanonicalSupported (),
'emailIsCorporate' => $emailCanonical ? -> isCorporate (),
'emailIsDisposable' => $emailCanonical ? -> isDisposable (),
'emailIsFree' => $emailCanonical ? -> isFree (),
2023-07-07 00:12:39 +00:00
]);
2025-11-02 08:59:28 +00:00
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2023-11-28 13:12:34 +00:00
try {
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'targets' , new Document ([
2024-02-16 04:07:16 +00:00
'$permissions' => [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
2023-11-28 13:12:34 +00:00
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2023-11-29 09:31:47 +00:00
'providerType' => MESSAGE_TYPE_EMAIL ,
2023-11-28 13:12:34 +00:00
'identifier' => $email ,
])));
$user -> setAttribute ( 'targets' , [ ... $user -> getAttribute ( 'targets' , []), $target ]);
} catch ( Duplicate ) {
$existingTarget = $dbForProject -> findOne ( 'targets' , [
Query :: equal ( 'identifier' , [ $email ]),
]);
2024-10-07 02:40:01 +00:00
if ( ! $existingTarget -> isEmpty ()) {
2024-06-11 15:47:25 +00:00
$user -> setAttribute ( 'targets' , $existingTarget , Document :: SET_TYPE_APPEND );
2024-06-05 18:04:01 +00:00
}
2023-11-28 13:12:34 +00:00
}
2023-12-15 05:24:37 +00:00
2023-12-15 04:45:25 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2023-08-23 01:34:23 +00:00
} catch ( Duplicate ) {
2022-07-22 14:38:06 +00:00
throw new Exception ( Exception :: USER_ALREADY_EXISTS );
2020-06-29 21:43:34 +00:00
}
2020-01-03 21:00:53 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> removeRole ( Role :: guests () -> toString ());
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
$authorization -> addRole ( Role :: users () -> toString ());
2020-11-20 21:02:26 +00:00
2022-09-07 11:02:36 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2022-09-07 11:07:40 +00:00
-> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-26 14:31:53 +00:00
});
2020-01-04 15:45:28 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Get account' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'get' ,
description : '/docs/references/account/get.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2024-03-06 18:07:58 +00:00
-> action ( function ( Response $response , Document $user ) {
if ( $user -> isEmpty ()) {
throw new Exception ( Exception :: USER_NOT_FOUND );
2020-12-27 11:57:42 +00:00
}
2024-03-06 18:07:58 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
});
2020-01-03 21:00:53 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/account' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Delete account' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'audits.event' , 'user.delete' )
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'delete' ,
description : '/docs/references/account/delete.md' ,
auth : [ AuthType :: ADMIN ],
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2024-03-06 18:07:58 +00:00
-> inject ( 'user' )
-> inject ( 'project' )
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'queueForEvents' )
-> inject ( 'queueForDeletes' )
-> action ( function ( Document $user , Document $project , Response $response , Database $dbForProject , Event $queueForEvents , Delete $queueForDeletes ) {
if ( $user -> isEmpty ()) {
throw new Exception ( Exception :: USER_NOT_FOUND );
2022-05-06 08:58:36 +00:00
}
2024-03-06 18:07:58 +00:00
if ( $project -> getId () === 'console' ) {
// get all memberships
$memberships = $user -> getAttribute ( 'memberships' , []);
foreach ( $memberships as $membership ) {
// prevent deletion if at least one active membership
if ( $membership -> getAttribute ( 'confirm' , false )) {
throw new Exception ( Exception :: USER_DELETION_PROHIBITED );
}
}
2020-01-03 21:00:53 +00:00
}
2021-08-05 05:06:38 +00:00
2024-03-06 18:07:58 +00:00
$dbForProject -> deleteDocument ( 'users' , $user -> getId ());
2021-06-17 09:33:57 +00:00
2024-03-06 18:07:58 +00:00
$queueForDeletes
-> setType ( DELETE_TYPE_DOCUMENT )
-> setDocument ( $user );
2021-08-05 05:06:38 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2023-07-07 00:12:39 +00:00
-> setParam ( 'userId' , $user -> getId ())
2024-03-06 18:07:58 +00:00
-> setPayload ( $response -> output ( $user , Response :: MODEL_USER ));
2022-04-04 06:30:07 +00:00
2024-03-06 18:07:58 +00:00
$response -> noContent ();
2020-12-26 14:31:53 +00:00
});
2020-01-03 21:00:53 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/sessions' )
2024-03-06 18:07:58 +00:00
-> desc ( 'List sessions' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-03-06 18:07:58 +00:00
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'listSessions' ,
description : '/docs/references/account/list-sessions.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_SESSION_LIST ,
)
],
contentType : ContentType :: JSON ,
))
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
2024-03-06 18:07:58 +00:00
-> inject ( 'user' )
-> inject ( 'locale' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> action ( function ( Response $response , User $user , Locale $locale , Store $store , ProofsToken $proofForToken ) {
2020-01-05 11:29:42 +00:00
2022-11-18 13:13:33 +00:00
2024-03-06 18:07:58 +00:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2025-11-04 03:48:57 +00:00
$current = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2023-05-29 13:58:45 +00:00
2024-03-06 18:07:58 +00:00
foreach ( $sessions as $key => $session ) { /** @var Document $session */
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2023-05-29 13:58:45 +00:00
2024-03-06 18:07:58 +00:00
$session -> setAttribute ( 'countryName' , $countryName );
$session -> setAttribute ( 'current' , ( $current == $session -> getId ()) ? true : false );
2025-01-10 18:21:04 +00:00
$session -> setAttribute ( 'secret' , $session -> getAttribute ( 'secret' , '' ));
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
$sessions [ $key ] = $session ;
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
$response -> dynamic ( new Document ([
'sessions' => $sessions ,
'total' => count ( $sessions ),
]), Response :: MODEL_SESSION_LIST );
});
2020-01-05 11:29:42 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/account/sessions' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Delete sessions' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'event' , 'users.[userId].sessions.[sessionId].delete' )
-> label ( 'audits.event' , 'session.delete' )
-> label ( 'audits.resource' , 'user/{user.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'deleteSessions' ,
description : '/docs/references/account/delete-sessions.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2024-03-06 18:07:58 +00:00
-> label ( 'abuse-limit' , 100 )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'locale' )
-> inject ( 'queueForEvents' )
-> inject ( 'queueForDeletes' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> action ( function ( Request $request , Response $response , User $user , Database $dbForProject , Locale $locale , Event $queueForEvents , Delete $queueForDeletes , Store $store , ProofsToken $proofForToken ) {
2020-06-29 21:43:34 +00:00
2024-03-06 18:07:58 +00:00
$protocol = $request -> getProtocol ();
$sessions = $user -> getAttribute ( 'sessions' , []);
2020-06-29 21:43:34 +00:00
2024-03-06 18:07:58 +00:00
foreach ( $sessions as $session ) { /** @var Document $session */
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
2021-08-31 07:04:17 +00:00
2024-03-06 18:07:58 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]));
}
$session
-> setAttribute ( 'current' , false )
-> setAttribute ( 'countryName' , $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' )));
2025-11-04 03:48:57 +00:00
if ( $proofForToken -> verify ( $store -> getProperty ( 'secret' , '' ), $session -> getAttribute ( 'secret' ))) {
2024-03-06 18:07:58 +00:00
$session -> setAttribute ( 'current' , true );
2024-03-07 23:27:28 +00:00
// If current session delete the cookies too
2024-03-06 18:07:58 +00:00
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ));
2024-03-06 18:07:58 +00:00
// Use current session for events.
$queueForEvents
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ));
$queueForDeletes
-> setType ( DELETE_TYPE_SESSION_TARGETS )
-> setDocument ( $session )
-> trigger ();
}
2021-08-31 07:04:17 +00:00
}
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-06-29 21:43:34 +00:00
2024-03-06 18:07:58 +00:00
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ());
$response -> noContent ();
2020-12-26 14:31:53 +00:00
});
2020-01-05 11:29:42 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/sessions/:sessionId' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Get session' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'getSession' ,
description : '/docs/references/account/get-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON
))
2024-03-06 18:07:58 +00:00
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to get the current device session.' )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
2024-03-06 18:07:58 +00:00
-> inject ( 'user' )
-> inject ( 'locale' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> action ( function ( ? string $sessionId , Response $response , User $user , Locale $locale , Store $store , ProofsToken $proofForToken ) {
2020-06-30 11:09:28 +00:00
2024-03-06 18:07:58 +00:00
$sessions = $user -> getAttribute ( 'sessions' , []);
$sessionId = ( $sessionId === 'current' )
2025-11-04 03:48:57 +00:00
? $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken )
2024-03-06 18:07:58 +00:00
: $sessionId ;
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
foreach ( $sessions as $session ) { /** @var Document $session */
if ( $sessionId === $session -> getId ()) {
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2020-06-30 11:09:28 +00:00
2024-03-06 18:07:58 +00:00
$session
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'current' , ( $proofForToken -> verify ( $store -> getProperty ( 'secret' , '' ), $session -> getAttribute ( 'secret' ))))
2024-03-06 18:07:58 +00:00
-> setAttribute ( 'countryName' , $countryName )
2025-01-10 18:21:04 +00:00
-> setAttribute ( 'secret' , $session -> getAttribute ( 'secret' , '' ))
2024-03-06 18:07:58 +00:00
;
2021-08-05 05:06:38 +00:00
2024-03-06 18:07:58 +00:00
return $response -> dynamic ( $session , Response :: MODEL_SESSION );
}
}
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2020-12-26 14:31:53 +00:00
});
2020-05-29 12:02:53 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/account/sessions/:sessionId' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Delete session' )
-> groups ([ 'api' , 'account' , 'mfa' ])
-> label ( 'scope' , 'account' )
-> label ( 'event' , 'users.[userId].sessions.[sessionId].delete' )
-> label ( 'audits.event' , 'session.delete' )
2022-08-15 17:04:23 +00:00
-> label ( 'audits.resource' , 'user/{user.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'deleteSession' ,
description : '/docs/references/account/delete-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2024-03-06 18:07:58 +00:00
-> label ( 'abuse-limit' , 100 )
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to delete the current device session.' )
-> inject ( 'requestTimestamp' )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2024-03-06 18:07:58 +00:00
-> inject ( 'locale' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-03-06 18:07:58 +00:00
-> inject ( 'queueForDeletes' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> action ( function ( ? string $sessionId , ? \DateTime $requestTimestamp , Request $request , Response $response , User $user , Database $dbForProject , Locale $locale , Event $queueForEvents , Delete $queueForDeletes , Store $store , ProofsToken $proofForToken ) {
2021-08-05 05:06:38 +00:00
2020-06-30 11:09:28 +00:00
$protocol = $request -> getProtocol ();
2024-03-06 18:07:58 +00:00
$sessionId = ( $sessionId === 'current' )
2025-11-04 03:48:57 +00:00
? $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken )
2024-03-06 18:07:58 +00:00
: $sessionId ;
2023-05-29 13:58:45 +00:00
2024-03-06 18:07:58 +00:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
foreach ( $sessions as $key => $session ) {
/** @var Document $session */
if ( $sessionId !== $session -> getId ()) {
continue ;
}
2020-01-05 11:29:42 +00:00
2025-04-30 05:40:47 +00:00
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
2023-07-15 00:28:45 +00:00
2024-03-06 18:07:58 +00:00
unset ( $sessions [ $key ]);
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
$session -> setAttribute ( 'current' , false );
2020-01-05 11:29:42 +00:00
2025-11-04 03:48:57 +00:00
if ( $proofForToken -> verify ( $store -> getProperty ( 'secret' , '' ), $session -> getAttribute ( 'secret' ))) { // If current session delete the cookies too
2024-03-06 18:07:58 +00:00
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' )));
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]));
}
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ));
2023-07-15 00:28:45 +00:00
}
2021-08-05 05:06:38 +00:00
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-08-05 05:06:38 +00:00
2024-03-06 18:07:58 +00:00
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ));
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
$queueForDeletes
-> setType ( DELETE_TYPE_SESSION_TARGETS )
-> setDocument ( $session )
-> trigger ();
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
$response -> noContent ();
return ;
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
});
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/sessions/:sessionId' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Update session' )
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
-> label ( 'event' , 'users.[userId].sessions.[sessionId].update' )
-> label ( 'audits.event' , 'session.update' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'updateSession' ,
description : '/docs/references/account/update-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON
))
2024-03-06 18:07:58 +00:00
-> label ( 'abuse-limit' , 10 )
-> param ( 'sessionId' , '' , new UID (), 'Session ID. Use the string \'current\' to update the current device session.' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'project' )
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> action ( function ( ? string $sessionId , Response $response , User $user , Database $dbForProject , Document $project , Event $queueForEvents , Store $store , ProofsToken $proofForToken ) {
2024-03-06 18:07:58 +00:00
$sessionId = ( $sessionId === 'current' )
2025-11-04 03:48:57 +00:00
? $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken )
2024-03-06 18:07:58 +00:00
: $sessionId ;
$sessions = $user -> getAttribute ( 'sessions' , []);
$session = null ;
foreach ( $sessions as $key => $value ) {
if ( $sessionId === $value -> getId ()) {
$session = $value ;
break ;
}
}
if ( $session === null ) {
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
}
// Extend session
2025-11-04 03:48:57 +00:00
$authDuration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2025-09-11 08:57:27 +00:00
$session -> setAttribute ( 'expire' , DateTime :: addSeconds ( new \DateTime (), $authDuration ));
2024-03-06 18:07:58 +00:00
// Refresh OAuth access token
$provider = $session -> getAttribute ( 'provider' , '' );
$refreshToken = $session -> getAttribute ( 'providerRefreshToken' , '' );
2025-12-04 16:37:16 +00:00
$oAuthProviders = Config :: getParam ( 'oAuthProviders' );
$className = $oAuthProviders [ $provider ][ 'class' ];
if ( ! \class_exists ( $className )) {
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
}
2024-03-06 18:07:58 +00:00
if ( ! empty ( $provider ) && \class_exists ( $className )) {
$appId = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
$oauth2 = new $className ( $appId , $appSecret , '' , [], []);
$oauth2 -> refreshTokens ( $refreshToken );
$session
-> setAttribute ( 'providerAccessToken' , $oauth2 -> getAccessToken ( '' ))
-> setAttribute ( 'providerRefreshToken' , $oauth2 -> getRefreshToken ( '' ))
2025-09-18 07:03:42 +00:00
-> setAttribute ( 'providerAccessTokenExpiry' , DateTime :: addSeconds ( new \DateTime (), ( int ) $oauth2 -> getAccessTokenExpiry ( '' )));
2024-03-06 18:07:58 +00:00
}
// Save changes
$dbForProject -> updateDocument ( 'sessions' , $sessionId , $session );
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ))
;
return $response -> dynamic ( $session , Response :: MODEL_SESSION );
2020-12-26 14:31:53 +00:00
});
2020-01-04 15:45:28 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/sessions/email' )
2022-07-07 22:23:52 +00:00
-> alias ( '/v1/account/sessions' )
2024-01-12 17:39:44 +00:00
-> desc ( 'Create email password session' )
2022-12-11 08:33:52 +00:00
-> groups ([ 'api' , 'account' , 'auth' , 'session' ])
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2025-01-10 03:12:10 +00:00
-> label ( 'auth.type' , 'email-password' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'session.create' )
2022-08-12 13:21:32 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'createEmailPasswordSession' ,
description : '/docs/references/account/create-session-email-password.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON
))
2020-01-04 15:45:28 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},email:{param-email}' )
2026-01-05 21:05:00 +00:00
-> label ( 'abuse-reset' , [ 201 ])
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2021-11-25 20:07:54 +00:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-10-31 14:54:15 +00:00
-> inject ( 'project' )
2025-12-17 08:56:45 +00:00
-> inject ( 'platform' )
2020-12-26 14:31:53 +00:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-06-24 13:12:09 +00:00
-> inject ( 'queueForMails' )
2024-01-05 11:31:38 +00:00
-> inject ( 'hooks' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $email , string $password , Request $request , Response $response , User $user , Database $dbForProject , Document $project , array $platform , Locale $locale , Reader $geodb , Event $queueForEvents , Mail $queueForMails , Hooks $hooks , Store $store , ProofsPassword $proofForPassword , ProofsToken $proofForToken , Authorization $authorization ) {
2021-06-03 13:03:51 +00:00
$email = \strtolower ( $email );
2020-06-30 11:09:28 +00:00
$protocol = $request -> getProtocol ();
2021-08-05 05:06:38 +00:00
2022-05-12 16:25:36 +00:00
$profile = $dbForProject -> findOne ( 'users' , [
2022-08-11 23:53:52 +00:00
Query :: equal ( 'email' , [ $email ]),
]);
2020-06-29 21:43:34 +00:00
2025-11-04 03:48:57 +00:00
$userProofForPassword = ProofsPassword :: createHash ( $profile -> getAttribute ( 'hash' , $proofForPassword -> getHash () -> getName ()), $profile -> getAttribute ( 'hashOptions' , $proofForPassword -> getHash () -> getOptions ()));
if ( $profile -> isEmpty () || empty ( $profile -> getAttribute ( 'passwordUpdate' )) || ! $userProofForPassword -> verify ( $password , $profile -> getAttribute ( 'password' ))) {
2022-08-14 16:23:30 +00:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-29 21:43:34 +00:00
}
2020-01-03 21:00:53 +00:00
2021-07-14 11:02:12 +00:00
if ( false === $profile -> getAttribute ( 'status' )) { // Account is blocked
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: USER_BLOCKED ); // User is in status blocked
2020-12-27 11:57:42 +00:00
}
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , false ]);
2022-10-31 14:54:15 +00:00
2025-11-04 03:48:57 +00:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2021-02-14 17:28:54 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2025-11-04 03:48:57 +00:00
$secret = $proofForToken -> generate ();
2021-02-14 17:28:54 +00:00
$session = new Document ( array_merge (
[
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2023-07-07 00:12:39 +00:00
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'provider' => SESSION_PROVIDER_EMAIL ,
2021-02-19 10:02:02 +00:00
'providerUid' => $email ,
2025-11-04 03:48:57 +00:00
'secret' => $proofForToken -> hash ( $secret ), // One way hash encryption to protect DB leak
2021-02-14 17:28:54 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
2024-02-21 17:18:56 +00:00
'factors' => [ 'password' ],
2021-02-14 17:28:54 +00:00
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2025-09-11 08:59:27 +00:00
'expire' => DateTime :: addSeconds ( new \DateTime (), $duration )
2022-05-23 14:54:50 +00:00
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
2021-02-14 17:28:54 +00:00
));
2020-10-30 19:53:27 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-01-03 21:00:53 +00:00
2022-05-06 08:58:36 +00:00
// Re-hash if not using recommended algo
2025-11-04 03:48:57 +00:00
if ( $user -> getAttribute ( 'hash' ) !== $proofForPassword -> getHash () -> getName ()) {
$proofForPasswordUpdated = new ProofsPassword ();
2023-07-07 00:12:39 +00:00
$user
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'password' , $proofForPasswordUpdated -> hash ( $password ))
-> setAttribute ( 'hash' , $proofForPasswordUpdated -> getHash () -> getName ())
-> setAttribute ( 'hashOptions' , $proofForPasswordUpdated -> getHash () -> getOptions ());
2023-07-07 00:12:39 +00:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-05-06 08:58:36 +00:00
}
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-01-04 15:45:28 +00:00
2022-08-02 09:21:53 +00:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
2023-07-07 00:12:39 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2020-01-11 13:58:02 +00:00
2025-11-04 03:48:57 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $secret )
-> encode ();
2020-06-29 21:43:34 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2025-11-04 03:48:57 +00:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ $store -> getKey () => $encoded ]));
2020-01-03 21:00:53 +00:00
}
2021-08-05 05:06:38 +00:00
2024-01-17 13:20:47 +00:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2020-06-29 21:43:34 +00:00
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2020-06-29 21:43:34 +00:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2020-07-02 21:48:02 +00:00
;
2020-10-30 19:53:27 +00:00
2022-05-23 14:54:50 +00:00
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
2021-06-17 09:33:57 +00:00
2020-10-30 19:53:27 +00:00
$session
-> setAttribute ( 'current' , true )
2021-06-17 09:33:57 +00:00
-> setAttribute ( 'countryName' , $countryName )
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'secret' , $encoded )
2020-10-30 19:53:27 +00:00
;
2021-08-05 05:06:38 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2023-07-07 00:12:39 +00:00
-> setParam ( 'userId' , $user -> getId ())
2022-04-04 06:30:07 +00:00
-> setParam ( 'sessionId' , $session -> getId ())
;
2024-07-16 13:42:46 +00:00
if ( $project -> getAttribute ( 'auths' , [])[ 'sessionAlerts' ] ? ? false ) {
2025-09-18 07:03:42 +00:00
if (
$dbForProject -> count ( 'sessions' , [
Query :: equal ( 'userId' , [ $user -> getId ()]),
]) !== 1
) {
2025-12-17 08:56:45 +00:00
sendSessionAlert ( $locale , $user , $project , $platform , $session , $queueForMails );
2024-07-16 12:03:26 +00:00
}
2024-06-24 13:12:09 +00:00
}
2021-07-25 14:47:18 +00:00
$response -> dynamic ( $session , Response :: MODEL_SESSION );
2020-12-26 14:31:53 +00:00
});
2020-01-03 21:00:53 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/sessions/anonymous' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Create anonymous session' )
-> groups ([ 'api' , 'account' , 'auth' , 'session' ])
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2024-03-06 18:07:58 +00:00
-> label ( 'auth.type' , 'anonymous' )
-> label ( 'audits.event' , 'session.create' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'createAnonymousSession' ,
description : '/docs/references/account/create-session-anonymous.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON
))
2020-01-05 11:29:42 +00:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2024-03-06 18:07:58 +00:00
-> inject ( 'locale' )
-> inject ( 'user' )
2020-12-26 14:31:53 +00:00
-> inject ( 'project' )
2024-03-06 18:07:58 +00:00
-> inject ( 'dbForProject' )
-> inject ( 'geodb' )
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( Request $request , Response $response , Locale $locale , User $user , Document $project , Database $dbForProject , Reader $geodb , Event $queueForEvents , Store $store , ProofsPassword $proofForPassword , ProofsToken $proofForToken , Authorization $authorization ) {
2024-02-20 11:45:11 +00:00
$protocol = $request -> getProtocol ();
2024-03-06 18:07:58 +00:00
if ( 'console' === $project -> getId ()) {
throw new Exception ( Exception :: USER_ANONYMOUS_CONSOLE_PROHIBITED , 'Failed to create anonymous user' );
2024-02-20 11:45:11 +00:00
}
2024-03-06 18:07:58 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2024-02-20 11:45:11 +00:00
2024-03-06 18:07:58 +00:00
if ( $limit !== 0 ) {
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2020-01-05 11:29:42 +00:00
2024-03-06 18:07:58 +00:00
if ( $total >= $limit ) {
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
}
2024-02-20 11:45:11 +00:00
}
2024-03-06 18:07:58 +00:00
$userId = ID :: unique ();
$user -> setAttributes ([
'$id' => $userId ,
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
],
'email' => null ,
'emailVerification' => false ,
'status' => true ,
'password' => null ,
2025-11-04 03:48:57 +00:00
'hash' => $proofForPassword -> getHash () -> getName (),
'hashOptions' => $proofForPassword -> getHash () -> getOptions (),
2024-03-06 18:07:58 +00:00
'passwordUpdate' => null ,
'registration' => DateTime :: now (),
'reset' => false ,
'name' => null ,
'mfa' => false ,
'prefs' => new \stdClass (),
'sessions' => null ,
'tokens' => null ,
'memberships' => null ,
'authenticators' => null ,
'search' => $userId ,
'accessedAt' => DateTime :: now (),
]);
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2024-02-20 11:45:11 +00:00
2024-03-06 18:07:58 +00:00
// Create session token
2025-11-04 03:48:57 +00:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2024-03-06 18:07:58 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2025-11-04 03:48:57 +00:00
$secret = $proofForToken -> generate ();
2024-02-20 11:45:11 +00:00
2024-03-06 18:07:58 +00:00
$session = new Document ( array_merge (
[
'$id' => ID :: unique (),
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'provider' => SESSION_PROVIDER_ANONYMOUS ,
'secret' => $proofForToken -> hash ( $secret ), // One way hash encryption to protect DB leak
2024-03-06 18:07:58 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
'factors' => [ 'anonymous' ],
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2025-09-11 09:18:23 +00:00
'expire' => DateTime :: addSeconds ( new \DateTime (), $duration )
2024-03-06 18:07:58 +00:00
],
$detector -> getOS (),
$detector -> getClient (),
$detector -> getDevice ()
));
2024-02-20 11:45:11 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2024-03-06 18:07:58 +00:00
2025-09-18 07:03:42 +00:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
2024-03-07 23:27:28 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
]));
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
;
2025-11-04 03:48:57 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $secret )
-> encode ();
2024-03-06 18:07:58 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2025-11-04 03:48:57 +00:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ $store -> getKey () => $encoded ]));
2024-02-20 11:45:11 +00:00
}
2024-03-06 18:07:58 +00:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2024-02-20 11:45:11 +00:00
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2024-03-06 18:07:58 +00:00
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
;
$countryName = $locale -> getText ( 'countries.' . strtolower ( $session -> getAttribute ( 'countryCode' )), $locale -> getText ( 'locale.country.unknown' ));
$session
-> setAttribute ( 'current' , true )
-> setAttribute ( 'countryName' , $countryName )
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'secret' , $encoded )
2024-03-06 18:07:58 +00:00
;
$response -> dynamic ( $session , Response :: MODEL_SESSION );
2024-02-20 11:45:11 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/sessions/token' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Create session' )
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
-> groups ([ 'api' , 'account' , 'session' ])
-> label ( 'scope' , 'sessions.write' )
-> label ( 'audits.event' , 'session.create' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'createSession' ,
description : '/docs/references/account/create-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON
))
2024-03-06 18:07:58 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'ip:{ip},userId:{param-userId}' )
2026-01-05 21:05:00 +00:00
-> label ( 'abuse-reset' , [ 201 ])
2024-03-06 18:07:58 +00:00
-> param ( 'userId' , '' , new CustomId (), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
-> param ( 'secret' , '' , new Text ( 256 ), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'project' )
2025-12-17 08:56:45 +00:00
-> inject ( 'platform' )
2024-03-06 18:07:58 +00:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
-> inject ( 'queueForEvents' )
2024-06-24 13:12:09 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2024-03-06 18:07:58 +00:00
-> action ( $createSession );
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/sessions/oauth2/:provider' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Create OAuth2 session' )
2024-02-20 11:45:11 +00:00
-> groups ([ 'api' , 'account' ])
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
-> label ( 'scope' , 'sessions.write' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'createOAuth2Session' ,
description : '/docs/references/account/create-session-oauth2.md' ,
type : MethodType :: WEBAUTH ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_MOVED_PERMANENTLY ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: HTML ,
2025-12-13 16:06:44 +00:00
hide : [ APP_SDK_PLATFORM_SERVER ],
2025-01-17 04:31:39 +00:00
))
2024-02-20 11:45:11 +00:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
2024-03-06 17:34:21 +00:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'oAuthProviders' )), true ), 'OAuth2 Provider. Currently, supported providers are: ' . \implode ( ', ' , \array_keys ( \array_filter ( Config :: getParam ( 'oAuthProviders' ), fn ( $node ) => ( ! $node [ 'mock' ])))) . '.' )
2025-12-07 20:29:45 +00:00
-> param ( 'success' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s 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 , [ 'redirectValidator' ])
-> param ( 'failure' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s 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 , [ 'redirectValidator' ])
2024-02-20 11:45:11 +00:00
-> param ( 'scopes' , [], new ArrayList ( new Text ( APP_LIMIT_ARRAY_ELEMENT_SIZE ), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.' , true )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
-> action ( function ( string $provider , string $success , string $failure , array $scopes , Request $request , Response $response , Document $project , array $platform ) use ( $oauthDefaultSuccess , $oauthDefaultFailure ) {
2025-07-07 22:32:04 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $request -> getHostname ();
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
2022-11-18 13:13:33 +00:00
2025-07-07 22:32:04 +00:00
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
2023-10-25 17:33:23 +00:00
$providerEnabled = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Enabled' ] ? ? false ;
2023-01-09 10:44:28 +00:00
2023-01-09 10:35:03 +00:00
if ( ! $providerEnabled ) {
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.' );
}
2023-01-09 10:44:28 +00:00
2023-10-25 17:33:23 +00:00
$appId = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
2020-01-05 11:29:42 +00:00
2020-06-29 21:43:34 +00:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
2024-04-01 11:02:47 +00:00
$key = System :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
2020-06-29 21:43:34 +00:00
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , \hex2bin ( $appSecret [ 'iv' ]), \hex2bin ( $appSecret [ 'tag' ]));
}
2020-01-05 11:29:42 +00:00
2020-06-29 21:43:34 +00:00
if ( empty ( $appId ) || empty ( $appSecret )) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please configure the provider app ID and app secret key from your ' . APP_NAME . ' console to continue.' );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2025-05-09 10:09:16 +00:00
$oAuthProviders = Config :: getParam ( 'oAuthProviders' );
2025-05-09 10:57:45 +00:00
$className = $oAuthProviders [ $provider ][ 'class' ];
2021-08-06 10:48:50 +00:00
if ( ! \class_exists ( $className )) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
2020-01-05 11:29:42 +00:00
}
2020-06-29 21:43:34 +00:00
2025-12-11 18:36:11 +00:00
$host = $platform [ 'consoleHostname' ] ? ? '' ;
2025-07-07 22:32:04 +00:00
$redirectBase = $protocol . '://' . $host ;
if ( $protocol === 'https' && $port !== '443' ) {
$redirectBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$redirectBase .= ':' . $port ;
}
2022-05-23 14:54:50 +00:00
if ( empty ( $success )) {
2025-07-07 22:32:04 +00:00
$success = $redirectBase . $oauthDefaultSuccess ;
2021-08-31 07:04:17 +00:00
}
2022-05-23 14:54:50 +00:00
if ( empty ( $failure )) {
2025-07-07 22:32:04 +00:00
$failure = $redirectBase . $oauthDefaultFailure ;
2021-08-31 07:04:17 +00:00
}
2024-01-09 16:38:29 +00:00
$oauth2 = new $className ( $appId , $appSecret , $callback , [
'success' => $success ,
'failure' => $failure ,
2024-03-06 18:07:58 +00:00
'token' => false ,
2024-01-09 16:38:29 +00:00
], $scopes );
2020-06-29 21:43:34 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $oauth2 -> getLoginURL ());
2020-12-26 14:31:53 +00:00
});
2020-01-05 11:29:42 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/sessions/oauth2/callback/:provider/:projectId' )
2025-04-14 18:40:48 +00:00
-> desc ( 'Get OAuth2 callback' )
2023-07-25 01:38:54 +00:00
-> groups ([ 'account' ])
2021-08-05 05:06:38 +00:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-01-05 11:29:42 +00:00
-> label ( 'scope' , 'public' )
-> label ( 'docs' , false )
2021-12-10 12:27:11 +00:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project ID.' )
2023-10-25 17:33:23 +00:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'oAuthProviders' )), true ), 'OAuth2 provider.' )
2023-08-22 20:11:33 +00:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.' , true )
2020-09-10 14:40:14 +00:00
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2023-07-28 00:12:15 +00:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-28 00:12:15 +00:00
-> action ( function ( string $projectId , string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response ) {
2025-07-07 22:32:04 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $request -> getHostname ();
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
2021-08-05 05:06:38 +00:00
2024-05-08 17:36:46 +00:00
$params = $request -> getParams ();
$params [ 'project' ] = $projectId ;
unset ( $params [ 'projectId' ]);
2020-06-29 21:43:34 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2025-07-07 22:32:04 +00:00
-> redirect ( $callbackBase . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
2024-05-08 17:36:46 +00:00
. \http_build_query ( $params ));
2020-12-26 14:31:53 +00:00
});
2020-01-05 11:29:42 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/sessions/oauth2/callback/:provider/:projectId' )
2025-04-14 18:40:48 +00:00
-> desc ( 'Create OAuth2 callback' )
2023-07-25 01:38:54 +00:00
-> groups ([ 'account' ])
2021-08-05 05:06:38 +00:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2020-05-29 12:02:53 +00:00
-> label ( 'scope' , 'public' )
-> label ( 'origin' , '*' )
-> label ( 'docs' , false )
2021-12-10 12:27:11 +00:00
-> param ( 'projectId' , '' , new Text ( 1024 ), 'Project ID.' )
2023-10-25 17:33:23 +00:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'oAuthProviders' )), true ), 'OAuth2 provider.' )
2023-08-22 20:11:33 +00:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.' , true )
2020-09-10 14:40:14 +00:00
-> param ( 'state' , '' , new Text ( 2048 ), 'Login state params.' , true )
2023-07-28 00:12:15 +00:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-28 00:12:15 +00:00
-> action ( function ( string $projectId , string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response ) {
2025-07-07 22:32:04 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $request -> getHostname ();
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
2021-08-05 05:06:38 +00:00
2024-05-08 17:36:46 +00:00
$params = $request -> getParams ();
$params [ 'project' ] = $projectId ;
unset ( $params [ 'projectId' ]);
2020-06-29 21:43:34 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
2025-07-07 22:32:04 +00:00
-> redirect ( $callbackBase . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
2024-05-08 17:36:46 +00:00
. \http_build_query ( $params ));
2020-12-26 14:31:53 +00:00
});
2020-05-29 12:02:53 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/sessions/oauth2/:provider/redirect' )
2025-04-14 18:40:48 +00:00
-> desc ( 'Get OAuth2 redirect' )
2022-12-11 08:33:52 +00:00
-> groups ([ 'api' , 'account' , 'session' ])
2021-08-05 05:06:38 +00:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2020-01-05 11:29:42 +00:00
-> label ( 'scope' , 'public' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'session.create' )
2022-08-15 17:04:23 +00:00
-> label ( 'audits.resource' , 'user/{user.$id}' )
2023-02-05 07:02:56 +00:00
-> label ( 'audits.userId' , '{user.$id}' )
2020-01-05 11:29:42 +00:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
-> label ( 'docs' , false )
2023-10-25 17:33:23 +00:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'oAuthProviders' )), true ), 'OAuth2 provider.' )
2023-08-22 20:11:33 +00:00
-> param ( 'code' , '' , new Text ( 2048 , 0 ), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.' , true )
2020-09-10 14:40:14 +00:00
-> param ( 'state' , '' , new Text ( 2048 ), 'OAuth2 state params.' , true )
2023-07-28 00:12:15 +00:00
-> param ( 'error' , '' , new Text ( 2048 , 0 ), 'Error code returned from the OAuth2 provider.' , true )
-> param ( 'error_description' , '' , new Text ( 2048 , 0 ), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.' , true )
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2025-12-07 20:29:45 +00:00
-> inject ( 'redirectValidator' )
2025-07-09 19:13:26 +00:00
-> inject ( 'devKey' )
2020-12-26 14:31:53 +00:00
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-02-09 12:45:18 +00:00
-> inject ( 'dbForPlatform' )
2020-12-26 14:31:53 +00:00
-> inject ( 'geodb' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2026-02-09 14:55:35 +00:00
-> action ( function ( string $provider , string $code , string $state , string $error , string $error_description , Request $request , Response $response , Document $project , Validator $redirectValidator , Document $devKey , User $user , Database $dbForProject , Database $dbForPlatform , Reader $geodb , Event $queueForEvents , Store $store , ProofsPassword $proofForPassword , ProofsToken $proofForToken , Authorization $authorization ) use ( $oauthDefaultSuccess ) {
2025-09-16 04:38:52 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $request -> getHostname ();
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
2020-06-29 21:43:34 +00:00
$defaultState = [ 'success' => $project -> getAttribute ( 'url' , '' ), 'failure' => '' ];
2023-10-25 17:33:23 +00:00
$appId = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
$providerEnabled = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Enabled' ] ? ? false ;
2023-01-10 04:52:21 +00:00
2025-12-04 16:37:16 +00:00
$oAuthProviders = Config :: getParam ( 'oAuthProviders' );
$className = $oAuthProviders [ $provider ][ 'class' ];
2021-08-06 10:48:50 +00:00
if ( ! \class_exists ( $className )) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2023-10-25 17:33:23 +00:00
$providers = Config :: getParam ( 'oAuthProviders' );
2023-07-15 00:28:45 +00:00
$providerName = $providers [ $provider ][ 'name' ] ? ? '' ;
/** @var Appwrite\Auth\OAuth2 $oauth2 */
2021-08-06 10:48:50 +00:00
$oauth2 = new $className ( $appId , $appSecret , $callback );
2020-01-05 11:29:42 +00:00
2020-06-29 21:43:34 +00:00
if ( ! empty ( $state )) {
try {
$state = \array_merge ( $defaultState , $oauth2 -> parseState ( $state ));
2024-02-08 01:17:54 +00:00
} catch ( \Throwable $exception ) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , 'Failed to parse login state params as passed from OAuth2 provider' );
2020-01-05 11:29:42 +00:00
}
2020-06-29 21:43:34 +00:00
} else {
$state = $defaultState ;
}
2020-01-05 11:29:42 +00:00
2026-02-09 12:45:18 +00:00
// Allow redirect to rule URL if related to project
2026-02-11 04:41:04 +00:00
// Check if $redirectValidator is instance of Redirect class
2026-02-09 14:55:35 +00:00
if ( $redirectValidator instanceof Redirect ) {
2026-02-11 04:41:04 +00:00
$domains = \array_filter ([
parse_url ( $state [ 'success' ], PHP_URL_HOST ) ? ? '' ,
parse_url ( $state [ 'failure' ], PHP_URL_HOST ) ? ? ''
], fn ( $domain ) => \is_string ( $domain ) && $domain !== '' );
if ( ! empty ( $domains )) {
$rules = $authorization -> skip ( fn () => $dbForPlatform -> find ( 'rules' , [
Query :: equal ( 'domain' , \array_values ( \array_unique ( $domains ))),
Query :: equal ( 'projectInternalId' , [ $project -> getSequence ()]),
Query :: limit ( 2 )
]));
foreach ( $rules as $rule ) {
$allowedHostnames = $redirectValidator -> getAllowedHostnames ();
$allowedHostnames [] = $rule -> getAttribute ( 'domain' , '' );
$redirectValidator -> setAllowedHostnames ( $allowedHostnames );
}
2026-02-09 14:55:35 +00:00
}
2026-02-09 12:45:18 +00:00
}
2025-12-07 20:29:45 +00:00
if ( $devKey -> isEmpty () && ! $redirectValidator -> isValid ( $state [ 'success' ])) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: PROJECT_INVALID_SUCCESS_URL );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2025-12-07 20:29:45 +00:00
if ( $devKey -> isEmpty () && ! empty ( $state [ 'failure' ]) && ! $redirectValidator -> isValid ( $state [ 'failure' ])) {
2022-08-15 07:13:24 +00:00
throw new Exception ( Exception :: PROJECT_INVALID_FAILURE_URL );
2020-06-29 21:43:34 +00:00
}
2023-07-15 00:28:45 +00:00
$failure = [];
if ( ! empty ( $state [ 'failure' ])) {
$failure = URLParser :: parse ( $state [ 'failure' ]);
}
$failureRedirect = ( function ( string $type , ? string $message = null , ? int $code = null ) use ( $failure , $response ) {
$exception = new Exception ( $type , $message , $code );
if ( ! empty ( $failure )) {
$query = URLParser :: parseQuery ( $failure [ 'query' ]);
$query [ 'error' ] = json_encode ([
'message' => $exception -> getMessage (),
'type' => $exception -> getType (),
'code' => ! \is_null ( $code ) ? $code : $exception -> getCode (),
]);
$failure [ 'query' ] = URLParser :: unparseQuery ( $query );
$response -> redirect ( URLParser :: unparse ( $failure ), 301 );
}
2021-08-05 05:06:38 +00:00
2023-07-15 00:28:45 +00:00
throw $exception ;
});
2020-01-05 11:29:42 +00:00
2023-07-15 00:28:45 +00:00
if ( ! $providerEnabled ) {
$failureRedirect ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.' );
}
2020-01-05 11:29:42 +00:00
2023-07-28 00:12:15 +00:00
if ( ! empty ( $error )) {
$message = 'The ' . $providerName . ' OAuth2 provider returned an error: ' . $error ;
if ( ! empty ( $error_description )) {
$message .= ': ' . $error_description ;
2020-01-05 11:29:42 +00:00
}
2023-07-28 00:12:15 +00:00
$failureRedirect ( Exception :: USER_OAUTH2_PROVIDER_ERROR , $message );
}
2020-01-05 11:29:42 +00:00
2023-07-28 00:12:15 +00:00
if ( empty ( $code )) {
$failureRedirect ( Exception :: USER_OAUTH2_PROVIDER_ERROR , 'Missing OAuth2 code. Please contact the Appwrite team for additional support.' );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2023-07-15 00:28:45 +00:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
2024-04-01 11:02:47 +00:00
$key = System :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
2023-07-15 00:28:45 +00:00
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , \hex2bin ( $appSecret [ 'iv' ]), \hex2bin ( $appSecret [ 'tag' ]));
2020-06-29 21:43:34 +00:00
}
2021-08-05 05:06:38 +00:00
2023-07-15 00:28:45 +00:00
$accessToken = '' ;
$refreshToken = '' ;
$accessTokenExpiry = 0 ;
2020-01-05 11:29:42 +00:00
2023-07-15 00:28:45 +00:00
try {
$accessToken = $oauth2 -> getAccessToken ( $code );
$refreshToken = $oauth2 -> getRefreshToken ( $code );
$accessTokenExpiry = $oauth2 -> getAccessTokenExpiry ( $code );
} catch ( OAuth2Exception $ex ) {
$failureRedirect (
$ex -> getType (),
'Failed to obtain access token. The ' . $providerName . ' OAuth2 provider returned an error: ' . $ex -> getMessage (),
$ex -> getCode (),
);
}
2020-01-05 11:29:42 +00:00
2023-07-15 00:28:45 +00:00
$oauth2ID = $oauth2 -> getUserID ( $accessToken );
if ( empty ( $oauth2ID )) {
$failureRedirect ( Exception :: USER_MISSING_ID );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2024-05-08 17:51:45 +00:00
$name = '' ;
$nameOAuth = $oauth2 -> getUserName ( $accessToken );
2025-09-15 10:22:45 +00:00
$userParam = $request -> getParam ( 'user' );
2024-05-08 17:51:45 +00:00
if ( ! empty ( $nameOAuth )) {
$name = $nameOAuth ;
2025-07-10 05:10:17 +00:00
} elseif ( $userParam !== null ) {
2025-07-14 23:05:58 +00:00
$userDecoded = \json_decode ( $userParam , true );
if ( isset ( $userDecoded [ 'name' ][ 'firstName' ]) && isset ( $userDecoded [ 'name' ][ 'lastName' ])) {
$name = $userDecoded [ 'name' ][ 'firstName' ] . ' ' . $userDecoded [ 'name' ][ 'lastName' ];
2024-05-08 17:51:45 +00:00
}
}
2023-05-18 01:11:45 +00:00
$email = $oauth2 -> getUserEmail ( $accessToken );
// Check if this identity is connected to a different user
if ( ! $user -> isEmpty ()) {
$userId = $user -> getId ();
2023-12-13 18:45:05 +00:00
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
2023-05-18 01:11:45 +00:00
Query :: equal ( 'providerEmail' , [ $email ]),
2025-05-26 05:42:11 +00:00
Query :: notEqual ( 'userInternalId' , $user -> getSequence ()),
2023-05-18 01:11:45 +00:00
]);
2024-10-07 02:40:01 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2025-04-23 14:39:55 +00:00
$failureRedirect ( Exception :: USER_ALREADY_EXISTS );
2023-12-13 18:45:05 +00:00
}
$userWithMatchingEmail = $dbForProject -> find ( 'users' , [
Query :: equal ( 'email' , [ $email ]),
Query :: notEqual ( '$id' , $userId ),
]);
if ( ! empty ( $userWithMatchingEmail )) {
2025-04-23 14:39:55 +00:00
$failureRedirect ( Exception :: USER_ALREADY_EXISTS );
2023-05-18 01:11:45 +00:00
}
2024-02-25 12:13:39 +00:00
$sessionUpgrade = true ;
2023-05-18 01:11:45 +00:00
}
2025-11-04 03:48:57 +00:00
$current = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2020-01-05 11:29:42 +00:00
2021-08-05 05:06:38 +00:00
if ( $current ) { // Delete current session of new one.
2022-04-04 09:59:32 +00:00
$currentDocument = $dbForProject -> getDocument ( 'sessions' , $current );
2022-05-23 14:54:50 +00:00
if ( ! $currentDocument -> isEmpty ()) {
2022-04-04 09:59:32 +00:00
$dbForProject -> deleteDocument ( 'sessions' , $currentDocument -> getId ());
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-05-06 22:31:05 +00:00
}
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2023-07-07 00:12:39 +00:00
if ( $user -> isEmpty ()) {
$session = $dbForProject -> findOne ( 'sessions' , [ // Get user by provider id
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
]);
2024-10-31 08:13:23 +00:00
if ( ! $session -> isEmpty ()) {
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $dbForProject -> getDocument ( 'users' , $session -> getAttribute ( 'userId' )) -> getArrayCopy ());
}
}
2020-01-05 11:29:42 +00:00
2021-07-17 21:21:33 +00:00
if ( $user === false || $user -> isEmpty ()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
2023-05-01 15:51:31 +00:00
if ( empty ( $email )) {
2025-04-23 14:39:55 +00:00
$failureRedirect ( Exception :: USER_UNAUTHORIZED , 'OAuth provider failed to return email.' );
2023-05-01 13:54:33 +00:00
}
2022-05-07 15:43:55 +00:00
$isVerified = $oauth2 -> isEmailVerified ( $accessToken );
2020-01-05 11:29:42 +00:00
2025-09-08 08:32:13 +00:00
$identity = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
2022-08-11 23:53:52 +00:00
]);
2025-09-08 08:32:13 +00:00
if ( ! $identity -> isEmpty ()) {
$user = $dbForProject -> getDocument ( 'users' , $identity -> getAttribute ( 'userId' ));
2023-07-07 00:12:39 +00:00
}
2020-01-05 11:29:42 +00:00
2025-12-18 11:46:03 +00:00
// If user is not found, check if there is a user with the same email
2023-05-18 01:11:45 +00:00
if ( $user === false || $user -> isEmpty ()) {
2025-09-08 08:32:13 +00:00
$userWithEmail = $dbForProject -> findOne ( 'users' , [
Query :: equal ( 'email' , [ $email ]),
2023-05-18 01:11:45 +00:00
]);
2025-09-08 08:32:13 +00:00
if ( ! $userWithEmail -> isEmpty ()) {
2025-12-18 11:46:03 +00:00
if ( ! $isVerified ) {
2025-12-18 11:49:59 +00:00
$failureRedirect ( Exception :: GENERAL_BAD_REQUEST );
2025-12-18 11:46:03 +00:00
}
2025-09-08 08:32:13 +00:00
$user -> setAttributes ( $userWithEmail -> getArrayCopy ());
2023-05-18 01:11:45 +00:00
}
}
2025-12-18 11:59:33 +00:00
// If user is not found, check if there is an identity with the same email
if ( $user === false || $user -> isEmpty ()) {
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
]);
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
if ( ! $isVerified ) {
$failureRedirect ( Exception :: GENERAL_BAD_REQUEST );
}
$user -> setAttributes ( $dbForProject -> getDocument ( 'users' , $identityWithMatchingEmail -> getAttribute ( 'userId' )) -> getArrayCopy ());
}
}
2023-05-18 01:11:45 +00:00
if ( $user === false || $user -> isEmpty ()) { // Last option -> create the user
2021-08-06 08:34:17 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-05 05:06:38 +00:00
2021-02-28 18:36:13 +00:00
if ( $limit !== 0 ) {
2022-05-16 09:58:17 +00:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-07-17 21:21:33 +00:00
2022-02-27 09:57:09 +00:00
if ( $total >= $limit ) {
2023-07-15 00:28:45 +00:00
$failureRedirect ( Exception :: USER_COUNT_EXCEEDED );
2021-02-28 18:36:13 +00:00
}
}
2021-08-05 05:06:38 +00:00
2025-11-10 15:30:40 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-10 15:30:40 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-10 15:30:40 +00:00
}
2020-06-29 21:43:34 +00:00
try {
2022-08-14 14:22:38 +00:00
$userId = ID :: unique ();
2023-07-07 00:12:39 +00:00
$user -> setAttributes ([
2022-08-14 14:22:38 +00:00
'$id' => $userId ,
2022-08-02 09:21:53 +00:00
'$permissions' => [
2022-08-14 05:21:11 +00:00
Permission :: read ( Role :: any ()),
2022-08-14 14:22:38 +00:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 09:21:53 +00:00
],
2020-06-29 21:43:34 +00:00
'email' => $email ,
2022-05-16 09:34:00 +00:00
'emailVerification' => true ,
2021-07-14 11:02:12 +00:00
'status' => true , // Email should already be authenticated by OAuth2 provider
2023-07-14 23:17:05 +00:00
'password' => null ,
2025-11-04 03:48:57 +00:00
'hash' => $proofForPassword -> getHash () -> getName (),
'hashOptions' => $proofForPassword -> getHash () -> getOptions (),
2022-07-04 09:55:11 +00:00
'passwordUpdate' => null ,
2022-07-13 14:02:49 +00:00
'registration' => DateTime :: now (),
2020-06-29 21:43:34 +00:00
'reset' => false ,
'name' => $name ,
2024-01-10 16:22:32 +00:00
'mfa' => false ,
2021-12-28 10:48:50 +00:00
'prefs' => new \stdClass (),
2022-04-26 10:36:49 +00:00
'sessions' => null ,
2022-04-27 11:06:53 +00:00
'tokens' => null ,
2022-04-27 12:44:47 +00:00
'memberships' => null ,
2024-02-29 20:59:49 +00:00
'authenticators' => null ,
2023-08-23 01:34:23 +00:00
'search' => implode ( ' ' , [ $userId , $email , $name ]),
'accessedAt' => DateTime :: now (),
2025-11-11 13:25:10 +00:00
'emailCanonical' => $emailCanonical ? -> getCanonical (),
'emailIsCanonical' => $emailCanonical ? -> isCanonicalSupported (),
'emailIsCorporate' => $emailCanonical ? -> isCorporate (),
'emailIsDisposable' => $emailCanonical ? -> isDisposable (),
'emailIsFree' => $emailCanonical ? -> isFree (),
2023-07-07 00:12:39 +00:00
]);
2025-11-10 15:30:40 +00:00
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$userDoc = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2023-11-14 19:54:55 +00:00
$dbForProject -> createDocument ( 'targets' , new Document ([
'$permissions' => [
2024-02-16 04:07:16 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
2023-11-14 19:54:55 +00:00
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
'userId' => $userDoc -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $userDoc -> getSequence (),
2023-11-29 04:05:37 +00:00
'providerType' => MESSAGE_TYPE_EMAIL ,
2023-11-14 19:54:55 +00:00
'identifier' => $email ,
]));
2023-08-23 01:34:23 +00:00
} catch ( Duplicate ) {
2023-07-15 00:28:45 +00:00
$failureRedirect ( Exception :: USER_ALREADY_EXISTS );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
}
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
$authorization -> addRole ( Role :: users () -> toString ());
2023-05-18 01:11:45 +00:00
2021-07-14 11:02:12 +00:00
if ( false === $user -> getAttribute ( 'status' )) { // Account is blocked
2023-07-15 00:28:45 +00:00
$failureRedirect ( Exception :: USER_BLOCKED ); // User is in status blocked
2020-12-27 11:57:42 +00:00
}
2023-05-18 01:11:45 +00:00
$identity = $dbForProject -> findOne ( 'identities' , [
2025-05-26 05:42:11 +00:00
Query :: equal ( 'userInternalId' , [ $user -> getSequence ()]),
2023-05-18 01:11:45 +00:00
Query :: equal ( 'provider' , [ $provider ]),
Query :: equal ( 'providerUid' , [ $oauth2ID ]),
]);
2024-10-31 08:13:23 +00:00
if ( $identity -> isEmpty ()) {
2023-05-18 01:11:45 +00:00
// Before creating the identity, check if the email is already associated with another user
$userId = $user -> getId ();
$identitiesWithMatchingEmail = $dbForProject -> find ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
2025-05-26 05:42:11 +00:00
Query :: notEqual ( 'userInternalId' , $user -> getSequence ()),
2023-05-18 01:11:45 +00:00
]);
if ( ! empty ( $identitiesWithMatchingEmail )) {
2025-04-23 14:39:55 +00:00
$failureRedirect ( Exception :: GENERAL_BAD_REQUEST ); /** Return a generic bad request to prevent exposing existing accounts */
2023-05-18 01:11:45 +00:00
}
$dbForProject -> createDocument ( 'identities' , new Document ([
'$id' => ID :: unique (),
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
],
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2023-05-18 01:11:45 +00:00
'userId' => $userId ,
'provider' => $provider ,
'providerUid' => $oauth2ID ,
'providerEmail' => $email ,
'providerAccessToken' => $accessToken ,
'providerRefreshToken' => $refreshToken ,
2025-09-18 07:03:42 +00:00
'providerAccessTokenExpiry' => DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ),
2023-05-18 01:11:45 +00:00
]));
} else {
$identity
-> setAttribute ( 'providerAccessToken' , $accessToken )
-> setAttribute ( 'providerRefreshToken' , $refreshToken )
2025-09-18 07:03:42 +00:00
-> setAttribute ( 'providerAccessTokenExpiry' , DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ));
2023-05-18 01:11:45 +00:00
$dbForProject -> updateDocument ( 'identities' , $identity -> getId (), $identity );
}
2023-06-23 00:46:09 +00:00
if ( empty ( $user -> getAttribute ( 'email' ))) {
$user -> setAttribute ( 'email' , $oauth2 -> getUserEmail ( $accessToken ));
2025-10-30 16:09:36 +00:00
2025-11-02 08:59:28 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $user -> getAttribute ( 'email' ));
2025-11-02 08:59:28 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-02 08:59:28 +00:00
}
2025-11-11 08:35:39 +00:00
2025-11-11 13:25:10 +00:00
$user -> setAttribute ( 'emailCanonical' , $emailCanonical ? -> getCanonical ());
$user -> setAttribute ( 'emailIsCanonical' , $emailCanonical ? -> isCanonicalSupported ());
$user -> setAttribute ( 'emailIsCorporate' , $emailCanonical ? -> isCorporate ());
$user -> setAttribute ( 'emailIsDisposable' , $emailCanonical ? -> isDisposable ());
$user -> setAttribute ( 'emailIsFree' , $emailCanonical ? -> isFree ());
2023-06-23 00:46:09 +00:00
}
2021-02-16 15:51:08 +00:00
2023-06-23 00:46:09 +00:00
if ( empty ( $user -> getAttribute ( 'name' ))) {
$user -> setAttribute ( 'name' , $oauth2 -> getUserName ( $accessToken ));
2021-02-16 15:51:08 +00:00
}
2024-01-09 16:38:29 +00:00
$user -> setAttribute ( 'status' , true );
2020-06-29 21:43:34 +00:00
2022-04-26 10:46:35 +00:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2020-06-29 21:43:34 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-06-29 21:43:34 +00:00
2024-01-09 16:38:29 +00:00
$state [ 'success' ] = URLParser :: parse ( $state [ 'success' ]);
$query = URLParser :: parseQuery ( $state [ 'success' ][ 'query' ]);
2022-04-26 10:46:35 +00:00
2025-11-04 03:48:57 +00:00
$duration = $project -> getAttribute ( 'auths' , [])[ 'duration' ] ? ? TOKEN_EXPIRATION_LOGIN_LONG ;
2024-01-09 16:38:29 +00:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), $duration ));
2021-07-17 21:21:33 +00:00
2025-11-10 11:38:47 +00:00
$proofForTokenOAuth2 = new ProofsToken ( TOKEN_LENGTH_OAUTH2 );
$proofForTokenOAuth2 -> setHash ( new Sha ());
2024-01-09 16:38:29 +00:00
// If the `token` param is set, we will return the token in the query string
if ( $state [ 'token' ]) {
2025-11-10 11:38:47 +00:00
$secret = $proofForTokenOAuth2 -> generate ();
2024-01-09 16:38:29 +00:00
$token = new Document ([
'$id' => ID :: unique (),
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_OAUTH2 ,
2025-11-10 11:38:47 +00:00
'secret' => $proofForTokenOAuth2 -> hash ( $secret ), // One way hash encryption to protect DB leak
2024-01-09 16:38:29 +00:00
'expire' => $expire ,
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2020-06-29 21:43:34 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2022-11-03 15:03:39 +00:00
2024-01-09 16:38:29 +00:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
-> setAttribute ( '$permissions' , [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
]));
2022-04-04 06:30:07 +00:00
2024-01-09 16:38:29 +00:00
$queueForEvents
-> setEvent ( 'users.[userId].tokens.[tokenId].create' )
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $token -> getId ())
;
2021-08-05 05:06:38 +00:00
2024-01-09 16:38:29 +00:00
$query [ 'secret' ] = $secret ;
$query [ 'userId' ] = $user -> getId ();
2023-09-28 12:45:52 +00:00
2024-03-06 17:34:21 +00:00
// If the `token` param is not set, we persist the session in a cookie
2024-01-09 16:38:29 +00:00
} else {
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$record = $geodb -> get ( $request -> getIP ());
2025-11-04 03:48:57 +00:00
$secret = $proofForToken -> generate ();
2024-01-09 16:41:42 +00:00
2024-01-09 16:38:29 +00:00
$session = new Document ( array_merge ([
'$id' => ID :: unique (),
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2024-01-09 16:38:29 +00:00
'provider' => $provider ,
'providerUid' => $oauth2ID ,
'providerAccessToken' => $accessToken ,
'providerRefreshToken' => $refreshToken ,
2025-11-04 03:48:57 +00:00
'providerAccessTokenExpiry' => DateTime :: addSeconds ( new \DateTime (), ( int ) $accessTokenExpiry ),
'secret' => $proofForToken -> hash ( $secret ), // One way hash encryption to protect DB leak
2024-01-09 16:38:29 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
2024-06-07 23:45:14 +00:00
'factors' => [ TYPE :: EMAIL , 'oauth2' ], // include a special oauth2 factor to bypass MFA checks
2024-01-09 16:38:29 +00:00
'countryCode' => ( $record ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ,
2025-09-11 09:18:23 +00:00
'expire' => DateTime :: addSeconds ( new \DateTime (), $duration )
2024-01-09 16:38:29 +00:00
], $detector -> getOS (), $detector -> getClient (), $detector -> getDevice ()));
2023-09-28 12:45:52 +00:00
2024-01-09 16:38:29 +00:00
$session = $dbForProject -> createDocument ( 'sessions' , $session -> setAttribute ( '$permissions' , [
2023-09-28 12:45:52 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
]));
2024-01-09 16:38:29 +00:00
$session -> setAttribute ( 'expire' , $expire );
2023-09-28 12:45:52 +00:00
2025-11-04 03:48:57 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $secret )
-> encode ();
2024-01-09 16:38:29 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
2025-11-04 03:48:57 +00:00
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([ $store -> getKey () => $encoded ]));
2024-01-09 16:38:29 +00:00
}
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'sessionId' , $session -> getId ())
-> setPayload ( $response -> output ( $session , Response :: MODEL_SESSION ))
;
2021-08-05 05:06:38 +00:00
2024-02-28 23:50:40 +00:00
// TODO: Remove this deprecated workaround - support only token
2024-01-09 16:38:29 +00:00
if ( $state [ 'success' ][ 'path' ] == $oauthDefaultSuccess ) {
$query [ 'project' ] = $project -> getId ();
$query [ 'domain' ] = Config :: getParam ( 'cookieDomain' );
2025-11-04 03:48:57 +00:00
$query [ 'key' ] = $store -> getKey ();
$query [ 'secret' ] = $encoded ;
2024-01-09 16:38:29 +00:00
}
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), $encoded , ( new \DateTime ( $expire )) -> getTimestamp (), '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ));
2020-06-29 21:43:34 +00:00
}
2024-02-25 12:13:39 +00:00
if ( isset ( $sessionUpgrade ) && $sessionUpgrade ) {
foreach ( $user -> getAttribute ( 'targets' , []) as $target ) {
if ( $target -> getAttribute ( 'providerType' ) !== MESSAGE_TYPE_PUSH ) {
continue ;
2024-03-06 18:07:58 +00:00
}
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
$target
-> setAttribute ( 'sessionId' , $session -> getId ())
2025-05-26 05:42:11 +00:00
-> setAttribute ( 'sessionInternalId' , $session -> getSequence ());
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
$dbForProject -> updateDocument ( 'targets' , $target -> getId (), $target );
2023-05-18 01:11:45 +00:00
}
}
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
$state [ 'success' ][ 'query' ] = URLParser :: unparseQuery ( $query );
$state [ 'success' ] = URLParser :: unparse ( $state [ 'success' ]);
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $state [ 'success' ])
;
2023-05-18 01:11:45 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/tokens/oauth2/:provider' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Create OAuth2 token' )
2023-05-18 01:11:45 +00:00
-> groups ([ 'api' , 'account' ])
2024-03-06 18:07:58 +00:00
-> label ( 'error' , __DIR__ . '/../../views/general/error.phtml' )
-> label ( 'scope' , 'sessions.write' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-14 18:40:48 +00:00
group : 'tokens' ,
2025-01-17 04:31:39 +00:00
name : 'createOAuth2Token' ,
description : '/docs/references/account/create-token-oauth2.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_MOVED_PERMANENTLY ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: HTML ,
type : MethodType :: WEBAUTH ,
))
2024-03-06 18:07:58 +00:00
-> label ( 'abuse-limit' , 50 )
-> label ( 'abuse-key' , 'ip:{ip}' )
2024-03-07 23:27:28 +00:00
-> param ( 'provider' , '' , new WhiteList ( \array_keys ( Config :: getParam ( 'oAuthProviders' )), true ), 'OAuth2 Provider. Currently, supported providers are: ' . \implode ( ', ' , \array_keys ( \array_filter ( Config :: getParam ( 'oAuthProviders' ), fn ( $node ) => ( ! $node [ 'mock' ])))) . '.' )
2025-12-07 20:29:45 +00:00
-> param ( 'success' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s 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 , [ 'redirectValidator' ])
-> param ( 'failure' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s 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 , [ 'redirectValidator' ])
2024-03-06 18:07:58 +00:00
-> param ( 'scopes' , [], new ArrayList ( new Text ( APP_LIMIT_ARRAY_ELEMENT_SIZE ), APP_LIMIT_ARRAY_PARAMS_SIZE ), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.' , true )
-> inject ( 'request' )
2023-05-18 01:11:45 +00:00
-> inject ( 'response' )
2024-03-06 18:07:58 +00:00
-> inject ( 'project' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
-> action ( function ( string $provider , string $success , string $failure , array $scopes , Request $request , Response $response , Document $project , array $platform ) use ( $oauthDefaultSuccess , $oauthDefaultFailure ) {
2025-12-16 14:14:57 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $request -> getHostname ();
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project -> getId ();
2024-03-06 18:07:58 +00:00
$providerEnabled = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Enabled' ] ? ? false ;
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
if ( ! $providerEnabled ) {
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.' );
2023-05-18 01:11:45 +00:00
}
2024-03-06 18:07:58 +00:00
$appId = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Appid' ] ? ? '' ;
$appSecret = $project -> getAttribute ( 'oAuthProviders' , [])[ $provider . 'Secret' ] ? ? '{}' ;
2023-05-18 01:11:45 +00:00
2024-03-06 18:07:58 +00:00
if ( ! empty ( $appSecret ) && isset ( $appSecret [ 'version' ])) {
2024-04-01 11:02:47 +00:00
$key = System :: getEnv ( '_APP_OPENSSL_KEY_V' . $appSecret [ 'version' ]);
2024-03-06 18:07:58 +00:00
$appSecret = OpenSSL :: decrypt ( $appSecret [ 'data' ], $appSecret [ 'method' ], $key , 0 , \hex2bin ( $appSecret [ 'iv' ]), \hex2bin ( $appSecret [ 'tag' ]));
}
2023-12-27 23:35:32 +00:00
2024-03-06 18:07:58 +00:00
if ( empty ( $appId ) || empty ( $appSecret )) {
throw new Exception ( Exception :: PROJECT_PROVIDER_DISABLED , 'This provider is disabled. Please configure the provider app ID and app secret key from your ' . APP_NAME . ' console to continue.' );
}
2025-12-04 16:37:16 +00:00
$oAuthProviders = Config :: getParam ( 'oAuthProviders' );
$className = $oAuthProviders [ $provider ][ 'class' ];
2024-03-06 18:07:58 +00:00
if ( ! \class_exists ( $className )) {
throw new Exception ( Exception :: PROJECT_PROVIDER_UNSUPPORTED );
}
2025-12-11 18:36:11 +00:00
$host = $platform [ 'consoleHostname' ] ? ? '' ;
2025-12-07 20:29:45 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) == 'disabled' ? 'http' : 'https' ;
$port = $request -> getPort ();
2025-07-07 22:32:04 +00:00
$redirectBase = $protocol . '://' . $host ;
if ( $protocol === 'https' && $port !== '443' ) {
$redirectBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$redirectBase .= ':' . $port ;
}
2024-03-06 18:07:58 +00:00
if ( empty ( $success )) {
2025-07-07 22:32:04 +00:00
$success = $redirectBase . $oauthDefaultSuccess ;
2024-03-06 18:07:58 +00:00
}
if ( empty ( $failure )) {
2025-07-07 22:32:04 +00:00
$failure = $redirectBase . $oauthDefaultFailure ;
2024-03-06 18:07:58 +00:00
}
$oauth2 = new $className ( $appId , $appSecret , $callback , [
'success' => $success ,
'failure' => $failure ,
'token' => true ,
], $scopes );
$response
-> addHeader ( 'Cache-Control' , 'no-store, no-cache, must-revalidate, max-age=0' )
-> addHeader ( 'Pragma' , 'no-cache' )
-> redirect ( $oauth2 -> getLoginURL ());
2023-05-18 01:11:45 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/tokens/magic-url' )
2023-11-30 11:35:52 +00:00
-> alias ( '/v1/account/sessions/magic-url' )
-> desc ( 'Create magic URL token' )
2024-02-12 01:18:19 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2021-08-31 09:22:48 +00:00
-> label ( 'auth.type' , 'magic-url' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'session.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-14 18:40:48 +00:00
group : 'tokens' ,
2025-01-17 04:31:39 +00:00
name : 'createMagicURLToken' ,
description : '/docs/references/account/create-token-magic-url.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
))
2024-02-12 01:18:19 +00:00
-> label ( 'abuse-limit' , 60 )
-> label ( 'abuse-key' , [ 'url:{url},email:{param-email}' , 'url:{url},ip:{ip}' ])
2025-08-18 16:18:12 +00:00
-> param ( 'userId' , '' , new CustomId (), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.' )
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2025-12-07 20:29:45 +00:00
-> param ( 'url' , '' , fn ( $redirectValidator ) => $redirectValidator , 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.' , true , [ 'redirectValidator' ])
2024-02-01 14:13:30 +00:00
-> param ( 'phrase' , false , new Boolean (), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.' , true )
2021-08-30 10:44:52 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2021-08-30 10:44:52 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2021-08-30 10:44:52 +00:00
-> inject ( 'locale' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2023-06-11 14:08:48 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForPassword' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $email , string $url , bool $phrase , Request $request , Response $response , Document $user , Document $project , Database $dbForProject , Locale $locale , Event $queueForEvents , Mail $queueForMails , ProofsPassword $proofForPassword , array $platform , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP disabled' );
2023-07-15 00:28:45 +00:00
}
2024-07-22 13:37:28 +00:00
$url = htmlentities ( $url );
2021-08-05 05:06:38 +00:00
2024-02-01 10:41:01 +00:00
if ( $phrase === true ) {
2024-02-01 14:13:30 +00:00
$phrase = Phrase :: generate ();
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2023-07-07 00:12:39 +00:00
$result = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'email' , [ $email ])]);
2024-10-31 08:13:23 +00:00
if ( ! $result -> isEmpty ()) {
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $result -> getArrayCopy ());
} else {
2021-10-07 19:10:43 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-30 10:44:52 +00:00
if ( $limit !== 0 ) {
2022-05-16 09:58:17 +00:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-08-30 10:44:52 +00:00
2022-02-27 09:57:09 +00:00
if ( $total >= $limit ) {
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-08-30 10:44:52 +00:00
}
}
2023-05-18 01:11:45 +00:00
// Makes sure this email is not already used in another identity
2023-12-13 18:45:05 +00:00
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
2023-05-18 01:11:45 +00:00
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2024-10-31 08:13:23 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2023-05-18 01:11:45 +00:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_EXISTS );
2023-12-13 18:45:05 +00:00
}
2023-04-11 23:01:50 +00:00
$userId = $userId === 'unique()' ? ID :: unique () : $userId ;
2021-08-30 10:44:52 +00:00
2025-11-10 15:30:40 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-10 15:30:40 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-10 15:30:40 +00:00
}
2023-07-07 00:12:39 +00:00
$user -> setAttributes ([
2022-08-14 14:22:38 +00:00
'$id' => $userId ,
2022-08-02 09:21:53 +00:00
'$permissions' => [
2022-08-14 05:21:11 +00:00
Permission :: read ( Role :: any ()),
2022-08-15 11:24:31 +00:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 09:21:53 +00:00
],
2021-12-16 11:30:43 +00:00
'email' => $email ,
'emailVerification' => false ,
'status' => true ,
'password' => null ,
2025-11-04 03:48:57 +00:00
'hash' => $proofForPassword -> getHash () -> getName (),
'hashOptions' => $proofForPassword -> getHash () -> getOptions (),
2022-07-04 09:55:11 +00:00
'passwordUpdate' => null ,
2022-07-13 14:02:49 +00:00
'registration' => DateTime :: now (),
2021-12-16 11:30:43 +00:00
'reset' => false ,
2024-01-10 16:22:32 +00:00
'mfa' => false ,
2021-12-28 10:48:50 +00:00
'prefs' => new \stdClass (),
2022-04-26 10:36:49 +00:00
'sessions' => null ,
2022-04-27 11:06:53 +00:00
'tokens' => null ,
2022-04-27 12:44:47 +00:00
'memberships' => null ,
2024-02-29 20:59:49 +00:00
'authenticators' => null ,
2023-08-23 01:34:23 +00:00
'search' => implode ( ' ' , [ $userId , $email ]),
'accessedAt' => DateTime :: now (),
2025-11-11 13:25:10 +00:00
'emailCanonical' => $emailCanonical ? -> getCanonical (),
'emailIsCanonical' => $emailCanonical ? -> isCanonicalSupported (),
'emailIsCorporate' => $emailCanonical ? -> isCorporate (),
'emailIsDisposable' => $emailCanonical ? -> isDisposable (),
'emailIsFree' => $emailCanonical ? -> isFree (),
2023-12-13 18:45:05 +00:00
]);
2023-07-07 00:12:39 +00:00
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2025-11-04 03:48:57 +00:00
$proofForToken = new ProofsToken ( TOKEN_LENGTH_MAGIC_URL );
$proofForToken -> setHash ( new Sha ());
$tokenSecret = $proofForToken -> generate ();
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_CONFIRM ));
2020-01-05 11:29:42 +00:00
2021-08-30 10:44:52 +00:00
$token = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2021-08-30 10:44:52 +00:00
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_MAGIC_URL ,
'secret' => $proofForToken -> hash ( $tokenSecret ), // One way hash encryption to protect DB leak
2022-11-04 14:48:29 +00:00
'expire' => $expire ,
2021-08-30 10:44:52 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2021-08-30 10:44:52 +00:00
2022-04-27 11:06:53 +00:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2021-08-30 10:44:52 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-08-30 10:44:52 +00:00
2022-05-23 14:54:50 +00:00
if ( empty ( $url )) {
2025-07-07 22:32:04 +00:00
$protocol = System :: getEnv ( '_APP_OPTIONS_FORCE_HTTPS' ) === 'disabled' ? 'http' : 'https' ;
2025-12-11 18:36:11 +00:00
$host = $platform [ 'consoleHostname' ] ? ? '' ;
2025-07-07 22:32:04 +00:00
$port = $request -> getPort ();
$callbackBase = $protocol . '://' . $host ;
if ( $protocol === 'https' && $port !== '443' ) {
$callbackBase .= ':' . $port ;
} elseif ( $protocol === 'http' && $port !== '80' ) {
$callbackBase .= ':' . $port ;
}
$url = $callbackBase . '/console/auth/magic-url' ;
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2021-08-30 10:44:52 +00:00
$url = Template :: parseURL ( $url );
2023-10-10 13:36:53 +00:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getId (), 'secret' => $tokenSecret , 'expire' => $expire , 'project' => $project -> getId ()]);
2021-08-30 10:44:52 +00:00
$url = Template :: unParseURL ( $url );
2022-12-14 06:35:04 +00:00
$subject = $locale -> getText ( " emails.magicSession.subject " );
2025-07-23 16:34:25 +00:00
$preview = $locale -> getText ( " emails.magicSession.preview " );
2023-04-19 08:29:29 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.magicSession-' . $locale -> default ] ? ? [];
2023-08-25 15:13:25 +00:00
2024-01-09 12:23:13 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$agentOs = $detector -> getOS ();
$agentClient = $detector -> getClient ();
$agentDevice = $detector -> getDevice ();
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-magic-url.tpl' );
2023-10-04 23:14:27 +00:00
$message
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.magicSession.hello " ))
2024-01-09 12:23:13 +00:00
-> setParam ( '{{optionButton}}' , $locale -> getText ( " emails.magicSession.optionButton " ))
-> setParam ( '{{buttonText}}' , $locale -> getText ( " emails.magicSession.buttonText " ))
-> setParam ( '{{optionUrl}}' , $locale -> getText ( " emails.magicSession.optionUrl " ))
-> setParam ( '{{clientInfo}}' , $locale -> getText ( " emails.magicSession.clientInfo " ))
2023-10-04 23:14:27 +00:00
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.magicSession.thanks " ))
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.magicSession.signature " ));
2024-01-10 14:52:32 +00:00
2024-02-01 10:41:01 +00:00
if ( ! empty ( $phrase )) {
2024-01-10 14:52:32 +00:00
$message -> setParam ( '{{securityPhrase}}' , $locale -> getText ( " emails.magicSession.securityPhrase " ));
} else {
$message -> setParam ( '{{securityPhrase}}' , '' );
2023-07-07 00:12:39 +00:00
}
2020-01-05 11:29:42 +00:00
2023-08-29 09:40:30 +00:00
$body = $message -> render ();
2022-05-16 09:34:00 +00:00
2023-08-29 09:40:30 +00:00
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
2020-01-05 11:29:42 +00:00
2024-04-01 11:02:47 +00:00
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
2025-01-14 09:09:37 +00:00
2023-08-29 09:40:30 +00:00
$replyTo = " " ;
2023-08-28 12:19:37 +00:00
2023-08-29 09:40:30 +00:00
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
2023-07-07 00:12:39 +00:00
}
2020-01-05 11:29:42 +00:00
2023-09-27 17:10:21 +00:00
$queueForMails
2023-08-25 15:13:25 +00:00
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
2023-08-29 09:40:30 +00:00
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
2023-05-18 01:11:45 +00:00
2023-08-30 04:30:44 +00:00
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
2023-05-18 01:11:45 +00:00
}
2023-08-30 04:30:44 +00:00
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
2023-05-18 01:11:45 +00:00
}
2023-09-27 17:10:21 +00:00
$queueForMails
2023-08-30 04:30:44 +00:00
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
2023-04-19 08:29:29 +00:00
}
2021-08-05 05:06:38 +00:00
2025-12-17 11:57:18 +00:00
$projectName = $project -> getAttribute ( 'name' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2023-08-27 22:45:37 +00:00
$emailVariables = [
'direction' => $locale -> getText ( 'settings.direction' ),
2024-02-23 00:46:13 +00:00
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
2024-02-22 12:47:01 +00:00
'user' => $user -> getAttribute ( 'name' ),
2025-12-17 11:57:18 +00:00
'project' => $projectName ,
2024-01-09 12:23:13 +00:00
'redirect' => $url ,
2024-02-22 12:47:01 +00:00
'agentDevice' => $agentDevice [ 'deviceBrand' ] ? ? $agentDevice [ 'deviceBrand' ] ? ? 'UNKNOWN' ,
'agentClient' => $agentClient [ 'clientName' ] ? ? 'UNKNOWN' ,
'agentOs' => $agentOs [ 'osName' ] ? ? 'UNKNOWN' ,
2024-02-23 00:46:13 +00:00
'phrase' => ! empty ( $phrase ) ? $phrase : '' ,
// TODO: remove unnecessary team variable from this email
'team' => '' ,
2023-08-27 22:45:37 +00:00
];
2021-07-17 21:21:33 +00:00
2023-06-11 14:08:48 +00:00
$queueForMails
2022-12-14 06:35:04 +00:00
-> setSubject ( $subject )
2025-07-23 16:34:25 +00:00
-> setPreview ( $preview )
2022-12-14 06:35:04 +00:00
-> setBody ( $body )
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2025-12-17 11:57:18 +00:00
-> setRecipient ( $email );
if ( $project -> getId () === 'console' ) {
$queueForMails -> setSenderName ( $platform [ 'emailSenderName' ]);
}
$queueForMails -> trigger ();
2021-08-05 05:06:38 +00:00
2024-05-22 02:11:06 +00:00
$token -> setAttribute ( 'secret' , $tokenSecret );
2022-12-18 04:38:27 +00:00
2024-04-03 03:36:53 +00:00
$queueForEvents
-> setPayload ( $response -> output ( $token , Response :: MODEL_TOKEN ), sensitive : [ 'secret' ]);
2021-08-30 10:44:52 +00:00
2024-02-01 10:41:01 +00:00
if ( ! empty ( $phrase )) {
$token -> setAttribute ( 'phrase' , $phrase );
2020-06-29 21:43:34 +00:00
}
2020-01-05 11:29:42 +00:00
2021-08-30 10:44:52 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2024-03-26 05:59:56 +00:00
-> dynamic ( $token , Response :: MODEL_TOKEN );
2021-08-30 10:44:52 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/tokens/email' )
2024-01-19 13:42:26 +00:00
-> desc ( 'Create email token (OTP)' )
2024-02-02 08:33:20 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2024-01-19 13:42:26 +00:00
-> label ( 'scope' , 'sessions.write' )
2024-02-02 08:33:20 +00:00
-> label ( 'auth.type' , 'email-otp' )
2024-01-19 13:42:26 +00:00
-> label ( 'audits.event' , 'session.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-12 11:01:12 +00:00
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-14 18:40:48 +00:00
group : 'tokens' ,
2025-01-17 04:31:39 +00:00
name : 'createEmailToken' ,
description : '/docs/references/account/create-token-email.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
))
2021-08-30 10:44:52 +00:00
-> label ( 'abuse-limit' , 10 )
2025-05-06 07:18:23 +00:00
-> label ( 'abuse-key' , [ 'url:{url},email:{param-email}' , 'url:{url},ip:{ip}' ])
2025-07-07 12:22:52 +00:00
-> param ( 'userId' , '' , new CustomId (), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.' )
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2024-02-01 14:13:30 +00:00
-> param ( 'phrase' , false , new Boolean (), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.' , true )
2021-08-30 10:44:52 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2022-10-31 14:54:15 +00:00
-> inject ( 'project' )
2025-12-17 11:35:08 +00:00
-> inject ( 'platform' )
2024-01-19 13:42:26 +00:00
-> inject ( 'dbForProject' )
2021-08-30 10:44:52 +00:00
-> inject ( 'locale' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-01-19 13:42:26 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForPassword' )
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $email , bool $phrase , Request $request , Response $response , User $user , Document $project , array $platform , Database $dbForProject , Locale $locale , Event $queueForEvents , Mail $queueForMails , ProofsPassword $proofForPassword , ProofsCode $proofForCode , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMTP_HOST' ))) {
2024-01-19 13:42:26 +00:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP disabled' );
}
2023-05-18 01:11:45 +00:00
2024-02-01 10:41:01 +00:00
if ( $phrase === true ) {
2024-02-01 14:13:30 +00:00
$phrase = Phrase :: generate ();
2020-12-27 11:57:42 +00:00
}
2024-01-19 13:42:26 +00:00
$result = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'email' , [ $email ])]);
2024-10-31 08:13:23 +00:00
if ( ! $result -> isEmpty ()) {
2024-01-19 13:42:26 +00:00
$user -> setAttributes ( $result -> getArrayCopy ());
} else {
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-30 10:44:52 +00:00
2024-01-19 13:42:26 +00:00
if ( $limit !== 0 ) {
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-08-30 10:44:52 +00:00
2024-01-19 13:42:26 +00:00
if ( $total >= $limit ) {
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
}
}
2021-08-30 10:44:52 +00:00
2024-01-19 13:42:26 +00:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
2023-05-18 01:11:45 +00:00
Query :: equal ( 'providerEmail' , [ $email ]),
]);
2024-10-31 08:13:23 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2024-03-01 13:20:32 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST ); /** Return a generic bad request to prevent exposing existing accounts */
2023-05-18 01:11:45 +00:00
}
2024-01-19 13:42:26 +00:00
$userId = $userId === 'unique()' ? ID :: unique () : $userId ;
2022-07-05 10:59:03 +00:00
2025-11-02 08:59:28 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-02 08:59:28 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-02 08:59:28 +00:00
}
2025-10-30 16:09:36 +00:00
2024-01-19 13:42:26 +00:00
$user -> setAttributes ([
'$id' => $userId ,
2023-05-18 01:11:45 +00:00
'$permissions' => [
Permission :: read ( Role :: any ()),
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
],
2024-01-19 13:42:26 +00:00
'email' => $email ,
'emailVerification' => false ,
'status' => true ,
'password' => null ,
2025-11-04 03:48:57 +00:00
'hash' => $proofForPassword -> getHash () -> getName (),
'hashOptions' => $proofForPassword -> getHash () -> getOptions (),
2024-01-19 13:42:26 +00:00
'passwordUpdate' => null ,
'registration' => DateTime :: now (),
'reset' => false ,
'prefs' => new \stdClass (),
'sessions' => null ,
'tokens' => null ,
'memberships' => null ,
'search' => implode ( ' ' , [ $userId , $email ]),
'accessedAt' => DateTime :: now (),
2025-11-11 13:25:10 +00:00
'emailCanonical' => $emailCanonical ? -> getCanonical (),
'emailIsCanonical' => $emailCanonical ? -> isCanonicalSupported (),
'emailIsCorporate' => $emailCanonical ? -> isCorporate (),
'emailIsDisposable' => $emailCanonical ? -> isDisposable (),
'emailIsFree' => $emailCanonical ? -> isFree (),
2024-01-19 13:42:26 +00:00
]);
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2025-07-29 06:27:57 +00:00
try {
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'targets' , new Document ([
2025-07-29 06:27:57 +00:00
'$permissions' => [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
'userId' => $user -> getId (),
'userInternalId' => $user -> getSequence (),
'providerType' => MESSAGE_TYPE_EMAIL ,
'identifier' => $email ,
])));
$user -> setAttribute ( 'targets' , [ ... $user -> getAttribute ( 'targets' , []), $target ]);
} catch ( Duplicate ) {
$existingTarget = $dbForProject -> findOne ( 'targets' , [
Query :: equal ( 'identifier' , [ $email ]),
]);
if ( ! $existingTarget -> isEmpty ()) {
$user -> setAttribute ( 'targets' , $existingTarget , Document :: SET_TYPE_APPEND );
}
}
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-12-27 11:57:42 +00:00
}
2025-11-04 03:48:57 +00:00
$tokenSecret = $proofForCode -> generate ();
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_OTP ));
2022-07-05 10:59:03 +00:00
2024-01-19 13:42:26 +00:00
$token = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2022-08-15 11:24:31 +00:00
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_EMAIL ,
'secret' => $proofForCode -> hash ( $tokenSecret ), // One way hash encryption to protect DB leak
2024-01-19 13:42:26 +00:00
'expire' => $expire ,
2020-07-03 15:14:51 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-29 21:43:34 +00:00
'ip' => $request -> getIP (),
2024-01-19 13:42:26 +00:00
]);
2020-06-29 21:43:34 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-06-29 21:43:34 +00:00
2024-01-19 13:42:26 +00:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2021-07-17 21:21:33 +00:00
2024-01-25 16:53:51 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-06-29 21:43:34 +00:00
2024-01-19 13:42:26 +00:00
$subject = $locale -> getText ( " emails.otpSession.subject " );
2025-07-22 11:13:17 +00:00
$preview = $locale -> getText ( " emails.otpSession.preview " );
2025-10-06 14:28:01 +00:00
$heading = $locale -> getText ( " emails.otpSession.heading " );
2022-11-03 15:03:39 +00:00
2024-01-19 13:42:26 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.otpSession-' . $locale -> default ] ? ? [];
2025-10-06 14:28:01 +00:00
$smtpBaseTemplate = $project -> getAttribute ( 'smtpBaseTemplate' , 'email-base' );
2025-10-06 16:22:18 +00:00
2025-10-07 10:37:47 +00:00
$validator = new FileName ();
if ( ! $validator -> isValid ( $smtpBaseTemplate )) {
2025-10-06 16:22:18 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST , 'Invalid template path' );
}
2025-10-06 14:28:01 +00:00
$bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl' ;
2022-11-03 15:03:39 +00:00
2024-01-19 13:42:26 +00:00
$detector = new Detector ( $request -> getUserAgent ( 'UNKNOWN' ));
$agentOs = $detector -> getOS ();
$agentClient = $detector -> getClient ();
$agentDevice = $detector -> getDevice ();
2022-04-04 06:30:07 +00:00
2024-01-19 13:42:26 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-otp.tpl' );
$message
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.otpSession.hello " ))
-> setParam ( '{{description}}' , $locale -> getText ( " emails.otpSession.description " ))
-> setParam ( '{{clientInfo}}' , $locale -> getText ( " emails.otpSession.clientInfo " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.otpSession.thanks " ))
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.otpSession.signature " ));
2024-02-01 10:41:01 +00:00
if ( ! empty ( $phrase )) {
2024-01-19 13:42:26 +00:00
$message -> setParam ( '{{securityPhrase}}' , $locale -> getText ( " emails.otpSession.securityPhrase " ));
} else {
$message -> setParam ( '{{securityPhrase}}' , '' );
2020-01-05 11:29:42 +00:00
}
2021-08-05 05:06:38 +00:00
2024-01-19 13:42:26 +00:00
$body = $message -> render ();
2020-06-29 21:43:34 +00:00
2024-01-19 13:42:26 +00:00
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
2020-01-05 11:29:42 +00:00
2024-04-01 11:02:47 +00:00
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
2024-01-19 13:42:26 +00:00
$replyTo = " " ;
2023-05-18 01:11:45 +00:00
2024-01-19 13:42:26 +00:00
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
}
2023-05-18 01:11:45 +00:00
2024-01-19 13:42:26 +00:00
$queueForMails
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
2023-05-18 01:11:45 +00:00
2024-01-19 13:42:26 +00:00
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
}
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
2023-05-18 01:11:45 +00:00
2024-01-19 13:42:26 +00:00
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
2023-05-18 01:11:45 +00:00
}
2024-01-19 13:42:26 +00:00
$queueForMails
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
2023-05-18 01:11:45 +00:00
}
2025-12-17 11:35:08 +00:00
$projectName = $project -> getAttribute ( 'name' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2024-01-19 13:42:26 +00:00
$emailVariables = [
2025-10-06 16:22:18 +00:00
'heading' => $heading ,
2024-01-19 13:42:26 +00:00
'direction' => $locale -> getText ( 'settings.direction' ),
2024-02-23 00:46:13 +00:00
// {{user}}, {{project}} and {{otp}} are required in the templates
2024-02-22 12:47:01 +00:00
'user' => $user -> getAttribute ( 'name' ),
2025-12-17 11:35:08 +00:00
'project' => $projectName ,
2024-01-19 13:42:26 +00:00
'otp' => $tokenSecret ,
2024-02-22 12:47:01 +00:00
'agentDevice' => $agentDevice [ 'deviceBrand' ] ? ? $agentDevice [ 'deviceBrand' ] ? ? 'UNKNOWN' ,
'agentClient' => $agentClient [ 'clientName' ] ? ? 'UNKNOWN' ,
'agentOs' => $agentOs [ 'osName' ] ? ? 'UNKNOWN' ,
2024-02-23 00:46:13 +00:00
'phrase' => ! empty ( $phrase ) ? $phrase : '' ,
// TODO: remove unnecessary team variable from this email
'team' => '' ,
2023-08-27 22:45:37 +00:00
];
2023-05-18 01:11:45 +00:00
2025-10-06 16:22:18 +00:00
if ( $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ) {
2025-09-24 07:36:20 +00:00
$emailVariables = array_merge ( $emailVariables , [
2025-12-17 11:35:08 +00:00
'accentColor' => $platform [ 'accentColor' ],
'logoUrl' => $platform [ 'logoUrl' ],
'twitter' => $platform [ 'twitterUrl' ],
'discord' => $platform [ 'discordUrl' ],
'github' => $platform [ 'githubUrl' ],
'terms' => $platform [ 'termsUrl' ],
'privacy' => $platform [ 'privacyUrl' ],
'platform' => $platform [ 'platformName' ],
2025-09-24 07:36:20 +00:00
]);
}
2023-06-11 14:08:48 +00:00
$queueForMails
2022-12-14 06:35:04 +00:00
-> setSubject ( $subject )
2025-07-22 11:13:17 +00:00
-> setPreview ( $preview )
2022-12-14 06:35:04 +00:00
-> setBody ( $body )
2025-09-15 19:28:23 +00:00
-> setBodyTemplate ( $bodyTemplate )
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2025-12-17 11:35:08 +00:00
-> setRecipient ( $email );
// since this is console project, set email sender name!
if ( $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ) {
$queueForMails -> setSenderName ( $platform [ 'emailSenderName' ]);
}
$queueForMails -> trigger ();
2023-05-18 01:11:45 +00:00
2024-05-22 02:11:06 +00:00
$token -> setAttribute ( 'secret' , $tokenSecret );
2024-04-03 03:36:53 +00:00
$queueForEvents
-> setPayload ( $response -> output ( $token , Response :: MODEL_TOKEN ), sensitive : [ 'secret' ]);
2021-08-30 10:44:52 +00:00
2024-02-01 10:41:01 +00:00
if ( ! empty ( $phrase )) {
$token -> setAttribute ( 'phrase' , $phrase );
2024-01-10 14:52:32 +00:00
}
2021-08-30 10:44:52 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2024-03-26 05:59:56 +00:00
-> dynamic ( $token , Response :: MODEL_TOKEN );
2023-05-18 01:11:45 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/sessions/magic-url' )
2024-02-24 12:53:47 +00:00
-> desc ( 'Update magic URL session' )
2023-11-30 11:35:52 +00:00
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2024-02-27 09:08:39 +00:00
-> groups ([ 'api' , 'account' , 'session' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2023-09-28 12:45:52 +00:00
-> label ( 'audits.event' , 'session.create' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'updateMagicURLSession' ,
description : '/docs/references/account/create-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON ,
2025-08-26 11:51:47 +00:00
deprecated : new Deprecated (
2025-08-26 12:03:49 +00:00
since : '1.6.0' ,
2025-08-26 11:51:47 +00:00
replaceWith : 'account.createSession'
),
2025-01-17 04:31:39 +00:00
))
2024-02-24 12:53:47 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'ip:{ip},userId:{param-userId}' )
2026-01-05 21:05:00 +00:00
-> label ( 'abuse-reset' , [ 201 ])
2024-02-24 12:53:47 +00:00
-> param ( 'userId' , '' , new CustomId (), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
-> inject ( 'request' )
2023-05-18 01:11:45 +00:00
-> inject ( 'response' )
2024-02-24 12:53:47 +00:00
-> inject ( 'user' )
2023-05-18 01:11:45 +00:00
-> inject ( 'dbForProject' )
2024-02-24 12:53:47 +00:00
-> inject ( 'project' )
2025-12-17 10:34:29 +00:00
-> inject ( 'platform' )
2024-02-24 12:53:47 +00:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
2023-12-27 23:35:32 +00:00
-> inject ( 'queueForEvents' )
2024-06-24 13:12:09 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( $userId , $secret , $request , $response , $user , $dbForProject , $project , $platform , $locale , $geodb , $queueForEvents , $queueForMails , $store , $proofForCode , $authorization ) use ( $createSession ) {
2025-11-04 03:48:57 +00:00
$proofForToken = new ProofsToken ( TOKEN_LENGTH_MAGIC_URL );
$proofForToken -> setHash ( new Sha ());
2026-01-14 15:08:00 +00:00
$createSession ( $userId , $secret , $request , $response , $user , $dbForProject , $project , $platform , $locale , $geodb , $queueForEvents , $queueForMails , $store , $proofForToken , $proofForCode , $authorization );
2025-11-04 03:48:57 +00:00
});
2023-05-18 01:11:45 +00:00
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/sessions/phone' )
2024-02-24 12:53:47 +00:00
-> desc ( 'Update phone session' )
-> label ( 'event' , 'users.[userId].sessions.[sessionId].create' )
2024-02-27 09:08:39 +00:00
-> groups ([ 'api' , 'account' , 'session' ])
2024-02-24 12:53:47 +00:00
-> label ( 'scope' , 'sessions.write' )
-> label ( 'audits.event' , 'session.create' )
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-12 06:50:02 +00:00
group : 'sessions' ,
2025-01-17 04:31:39 +00:00
name : 'updatePhoneSession' ,
description : '/docs/references/account/create-session.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_SESSION ,
)
],
contentType : ContentType :: JSON ,
2025-08-26 11:51:47 +00:00
deprecated : new Deprecated (
2025-08-26 12:03:49 +00:00
since : '1.6.0' ,
2025-08-26 11:51:47 +00:00
replaceWith : 'account.createSession'
),
2025-01-17 04:31:39 +00:00
))
2023-09-28 12:45:52 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'ip:{ip},userId:{param-userId}' )
2024-01-12 17:26:01 +00:00
-> param ( 'userId' , '' , new CustomId (), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
2024-03-06 18:07:58 +00:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
2024-01-09 15:56:01 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'project' )
2025-12-17 10:34:29 +00:00
-> inject ( 'platform' )
2024-01-09 15:56:01 +00:00
-> inject ( 'locale' )
-> inject ( 'geodb' )
-> inject ( 'queueForEvents' )
2024-06-24 13:12:09 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
2024-01-09 15:56:01 +00:00
-> action ( $createSession );
2023-05-18 01:11:45 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/tokens/phone' )
2023-11-30 11:35:52 +00:00
-> alias ( '/v1/account/sessions/phone' )
-> desc ( 'Create phone token' )
2025-03-05 17:42:43 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2022-06-08 09:00:38 +00:00
-> label ( 'auth.type' , 'phone' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'session.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-14 18:40:48 +00:00
group : 'tokens' ,
2025-01-17 04:31:39 +00:00
name : 'createPhoneToken' ,
description : '/docs/references/account/create-token-phone.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
))
2022-06-08 09:00:38 +00:00
-> label ( 'abuse-limit' , 10 )
2024-02-12 01:18:19 +00:00
-> label ( 'abuse-key' , [ 'url:{url},phone:{param-phone}' , 'url:{url},ip:{ip}' ])
2025-08-18 16:18:12 +00:00
-> param ( 'userId' , '' , new CustomId (), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the phone number has never been used, a new account is created using the provided userId. Otherwise, if the phone number is already attached to an account, the user ID is ignored.' )
2022-08-14 15:10:12 +00:00
-> param ( 'phone' , '' , new Phone (), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.' )
2021-08-30 10:44:52 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2021-08-30 10:44:52 +00:00
-> inject ( 'project' )
2025-12-17 11:35:08 +00:00
-> inject ( 'platform' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
-> inject ( 'queueForMessaging' )
2023-04-19 08:29:29 +00:00
-> inject ( 'locale' )
2025-01-08 11:31:01 +00:00
-> inject ( 'timelimit' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2025-01-08 11:31:01 +00:00
-> inject ( 'plan' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $phone , Request $request , Response $response , User $user , Document $project , array $platform , Database $dbForProject , Event $queueForEvents , Messaging $queueForMessaging , Locale $locale , callable $timelimit , StatsUsage $queueForStatsUsage , array $plan , Store $store , ProofsCode $proofForCode , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMS_PROVIDER' ))) {
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: GENERAL_PHONE_DISABLED , 'Phone provider not configured' );
2021-08-31 07:32:58 +00:00
}
2023-07-07 00:12:39 +00:00
$result = $dbForProject -> findOne ( 'users' , [ Query :: equal ( 'phone' , [ $phone ])]);
2024-10-31 08:13:23 +00:00
if ( ! $result -> isEmpty ()) {
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $result -> getArrayCopy ());
} else {
2021-10-07 19:10:43 +00:00
$limit = $project -> getAttribute ( 'auths' , [])[ 'limit' ] ? ? 0 ;
2021-08-30 10:44:52 +00:00
if ( $limit !== 0 ) {
2022-05-16 09:58:17 +00:00
$total = $dbForProject -> count ( 'users' , max : APP_LIMIT_USERS );
2021-08-30 10:44:52 +00:00
2022-02-27 09:57:09 +00:00
if ( $total >= $limit ) {
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_COUNT_EXCEEDED );
2021-08-30 10:44:52 +00:00
}
}
2022-08-14 14:22:38 +00:00
$userId = $userId == 'unique()' ? ID :: unique () : $userId ;
2023-07-07 00:12:39 +00:00
$user -> setAttributes ([
2022-08-14 14:22:38 +00:00
'$id' => $userId ,
2022-08-02 09:21:53 +00:00
'$permissions' => [
2022-08-14 05:21:11 +00:00
Permission :: read ( Role :: any ()),
2022-08-15 11:24:31 +00:00
Permission :: update ( Role :: user ( $userId )),
Permission :: delete ( Role :: user ( $userId )),
2022-08-02 09:21:53 +00:00
],
2022-06-08 09:00:38 +00:00
'email' => null ,
2022-08-14 13:43:41 +00:00
'phone' => $phone ,
2021-12-16 11:30:43 +00:00
'emailVerification' => false ,
2022-06-08 09:00:38 +00:00
'phoneVerification' => false ,
2021-12-16 11:30:43 +00:00
'status' => true ,
'password' => null ,
2022-07-04 09:55:11 +00:00
'passwordUpdate' => null ,
2022-07-13 14:02:49 +00:00
'registration' => DateTime :: now (),
2021-12-16 11:30:43 +00:00
'reset' => false ,
2021-12-28 10:48:50 +00:00
'prefs' => new \stdClass (),
2022-04-26 10:36:49 +00:00
'sessions' => null ,
2022-04-27 11:06:53 +00:00
'tokens' => null ,
2022-04-27 12:44:47 +00:00
'memberships' => null ,
2023-08-23 01:34:23 +00:00
'search' => implode ( ' ' , [ $userId , $phone ]),
'accessedAt' => DateTime :: now (),
2025-11-10 15:30:40 +00:00
'emailCanonical' => null ,
'emailIsCanonical' => null ,
'emailIsCorporate' => null ,
'emailIsDisposable' => null ,
'emailIsFree' => null ,
2023-07-07 00:12:39 +00:00
]);
2025-05-26 05:42:11 +00:00
$user -> removeAttribute ( '$sequence' );
2026-01-14 15:08:00 +00:00
$user = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'users' , $user ));
2023-11-28 13:12:34 +00:00
try {
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> createDocument ( 'targets' , new Document ([
2024-02-16 04:07:16 +00:00
'$permissions' => [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
2023-11-28 13:12:34 +00:00
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2023-11-29 09:52:26 +00:00
'providerType' => MESSAGE_TYPE_SMS ,
2023-11-28 13:12:34 +00:00
'identifier' => $phone ,
])));
$user -> setAttribute ( 'targets' , [ ... $user -> getAttribute ( 'targets' , []), $target ]);
} catch ( Duplicate ) {
$existingTarget = $dbForProject -> findOne ( 'targets' , [
Query :: equal ( 'identifier' , [ $phone ]),
]);
2024-10-07 02:40:01 +00:00
$user -> setAttribute ( 'targets' , [ ... $user -> getAttribute ( 'targets' , []), $existingTarget -> isEmpty () ? false : $existingTarget ]);
2023-11-28 13:12:34 +00:00
}
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-08-30 10:44:52 +00:00
}
2024-06-16 07:28:22 +00:00
$secret = null ;
$sendSMS = true ;
$mockNumbers = $project -> getAttribute ( 'auths' , [])[ 'mockNumbers' ] ? ? [];
foreach ( $mockNumbers as $mockNumber ) {
if ( $mockNumber [ 'phone' ] === $phone ) {
$secret = $mockNumber [ 'otp' ];
$sendSMS = false ;
break ;
}
}
2025-11-04 03:48:57 +00:00
$secret ? ? = $proofForCode -> generate ();
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_OTP ));
2021-10-07 19:10:43 +00:00
2021-08-30 10:44:52 +00:00
$token = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2021-08-30 10:44:52 +00:00
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_PHONE ,
'secret' => $proofForCode -> hash ( $secret ),
2022-11-04 14:48:29 +00:00
'expire' => $expire ,
2021-08-30 10:44:52 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2021-08-30 10:44:52 +00:00
2022-04-27 11:06:53 +00:00
$token = $dbForProject -> createDocument ( 'tokens' , $token
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2021-08-30 10:44:52 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-08-30 10:44:52 +00:00
2024-06-16 07:28:22 +00:00
if ( $sendSMS ) {
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/sms-base.tpl' );
2023-04-19 08:44:22 +00:00
2024-06-16 07:28:22 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'sms.login-' . $locale -> default ] ? ? [];
if ( ! empty ( $customTemplate )) {
$message = $customTemplate [ 'message' ] ? ? $message ;
}
2025-12-17 11:35:08 +00:00
$projectName = $project -> getAttribute ( 'name' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2024-06-20 15:01:20 +00:00
$messageContent = Template :: fromString ( $locale -> getText ( " sms.verification.body " ));
$messageContent
2025-12-17 11:35:08 +00:00
-> setParam ( '{{project}}' , $projectName )
2024-06-20 15:01:20 +00:00
-> setParam ( '{{secret}}' , $secret );
$messageContent = \strip_tags ( $messageContent -> render ());
$message = $message -> setParam ( '{{token}}' , $messageContent );
2024-06-16 07:28:22 +00:00
$message = $message -> render ();
2024-06-20 15:01:20 +00:00
$messageDoc = new Document ([
'$id' => $token -> getId (),
'data' => [
'content' => $message ,
],
]);
2024-06-16 07:28:22 +00:00
$queueForMessaging
2024-06-20 15:01:20 +00:00
-> setType ( MESSAGE_SEND_TYPE_INTERNAL )
-> setMessage ( $messageDoc )
-> setRecipients ([ $phone ])
-> setProviderType ( MESSAGE_TYPE_SMS );
2025-01-08 11:31:01 +00:00
2026-01-15 12:59:30 +00:00
$helper = PhoneNumberUtil :: getInstance ();
2026-01-15 13:19:34 +00:00
try {
$countryCode = $helper -> parse ( $phone ) -> getCountryCode ();
2025-01-08 11:31:01 +00:00
2026-01-15 13:19:34 +00:00
if ( ! empty ( $countryCode )) {
$queueForStatsUsage
-> addMetric ( str_replace ( '{countryCode}' , $countryCode , METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE ), 1 );
}
} catch ( NumberParseException $e ) {
// Ignore invalid phone number for country code stats
2025-01-08 11:31:01 +00:00
}
2026-01-15 12:59:30 +00:00
$queueForStatsUsage
-> addMetric ( METRIC_AUTH_METHOD_PHONE , 1 )
-> setProject ( $project )
-> trigger ();
2021-08-31 09:22:48 +00:00
}
2024-05-22 02:11:06 +00:00
$token -> setAttribute ( 'secret' , $secret );
2023-08-28 12:19:37 +00:00
2024-04-03 03:36:53 +00:00
$queueForEvents
-> setPayload ( $response -> output ( $token , Response :: MODEL_TOKEN ), sensitive : [ 'secret' ]);
2025-01-10 13:47:38 +00:00
// Encode secret for clients
2025-11-04 03:48:57 +00:00
$encoded = $store
-> setProperty ( 'id' , $user -> getId ())
-> setProperty ( 'secret' , $secret )
-> encode ();
$token -> setAttribute ( 'secret' , $encoded );
2022-06-08 09:00:38 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2024-03-26 05:59:56 +00:00
-> dynamic ( $token , Response :: MODEL_TOKEN );
2022-06-08 09:00:38 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/jwts' )
2024-05-27 20:04:50 +00:00
-> alias ( '/v1/account/jwt' )
2022-11-03 15:24:32 +00:00
-> desc ( 'Create JWT' )
2021-02-28 18:36:13 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2021-02-28 18:36:13 +00:00
-> label ( 'auth.type' , 'jwt' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-04-14 18:40:48 +00:00
group : 'tokens' ,
2025-01-17 04:31:39 +00:00
name : 'createJWT' ,
description : '/docs/references/account/create-jwt.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_JWT ,
)
],
contentType : ContentType :: JSON ,
))
2025-12-22 17:08:48 +00:00
-> param ( 'duration' , 900 , new Range ( 0 , 3600 ), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.' , true )
2025-12-23 14:42:14 +00:00
-> label ( 'abuse-limit' , APP_LIMIT_WRITE_RATE_DEFAULT * 2 )
-> label ( 'abuse-time' , APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT )
2021-07-27 11:07:39 +00:00
-> label ( 'abuse-key' , 'url:{url},userId:{userId}' )
2020-12-28 17:03:27 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
2025-12-22 17:27:29 +00:00
-> action ( function ( int $duration , Response $response , User $user , Store $store , ProofsToken $proofForToken ) {
2025-11-04 03:48:57 +00:00
$sessionId = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2022-04-26 08:52:59 +00:00
2025-11-04 03:48:57 +00:00
if ( ! $sessionId ) {
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_SESSION_NOT_FOUND );
2020-12-28 17:03:27 +00:00
}
2021-08-05 05:06:38 +00:00
2025-12-22 17:27:29 +00:00
$jwt = new JWT ( System :: getEnv ( '_APP_OPENSSL_KEY_V1' ), 'HS256' , $duration , 0 );
2020-12-28 17:03:27 +00:00
2022-09-07 11:02:36 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2025-09-18 07:03:42 +00:00
-> dynamic ( new Document ([
'jwt' => $jwt -> encode ([
'userId' => $user -> getId (),
2025-11-04 03:48:57 +00:00
'sessionId' => $sessionId ,
2025-09-18 07:03:42 +00:00
])
]), Response :: MODEL_JWT );
2020-12-28 17:03:27 +00:00
});
2020-01-05 11:29:42 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/prefs' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Get account preferences' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'getPrefs' ,
description : '/docs/references/account/get-prefs.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_PREFERENCES ,
)
],
contentType : ContentType :: JSON
))
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2022-08-10 08:45:10 +00:00
-> action ( function ( Response $response , Document $user ) {
2020-01-31 22:34:07 +00:00
2023-04-12 16:02:43 +00:00
$prefs = $user -> getAttribute ( 'prefs' , []);
2020-06-29 21:43:34 +00:00
2021-07-25 14:47:18 +00:00
$response -> dynamic ( new Document ( $prefs ), Response :: MODEL_PREFERENCES );
2020-12-26 14:31:53 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/logs' )
2023-08-01 15:26:48 +00:00
-> desc ( 'List logs' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'logs' ,
2025-01-17 04:31:39 +00:00
name : 'listLogs' ,
description : '/docs/references/account/list-logs.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_LOG_LIST ,
)
],
contentType : ContentType :: JSON ,
))
2023-05-16 12:56:20 +00:00
-> param ( 'queries' , [], new Queries ([ new Limit (), new Offset ()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset' , true )
2025-11-05 02:16:37 +00:00
-> param ( 'total' , true , new Boolean ( true ), 'When set to false, the total count returned will be 0 and will not be calculated.' , true )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'locale' )
-> inject ( 'geodb' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2025-12-14 07:50:21 +00:00
-> inject ( 'audit' )
-> action ( function ( array $queries , bool $includeTotal , Response $response , Document $user , Locale $locale , Reader $geodb , Database $dbForProject , Audit $audit ) {
2020-06-29 21:43:34 +00:00
2024-02-12 16:02:04 +00:00
try {
$queries = Query :: parseQueries ( $queries );
} catch ( QueryException $e ) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $e -> getMessage ());
}
2025-12-14 07:50:21 +00:00
$grouped = Query :: groupByType ( $queries );
$limit = $grouped [ 'limit' ] ? ? 25 ;
$offset = $grouped [ 'offset' ] ? ? 0 ;
$logs = $audit -> getLogsByUser ( $user -> getSequence (), offset : $offset , limit : $limit );
2020-06-29 21:43:34 +00:00
$output = [];
2023-08-28 12:19:37 +00:00
2020-06-29 21:43:34 +00:00
foreach ( $logs as $i => & $log ) {
$log [ 'userAgent' ] = ( ! empty ( $log [ 'userAgent' ])) ? $log [ 'userAgent' ] : 'UNKNOWN' ;
2023-08-28 12:19:37 +00:00
2021-02-14 17:28:54 +00:00
$detector = new Detector ( $log [ 'userAgent' ]);
2023-08-30 04:30:44 +00:00
2021-11-18 10:33:42 +00:00
$output [ $i ] = new Document ( array_merge (
$log -> getArrayCopy (),
$log [ 'data' ],
$detector -> getOS (),
2024-03-06 18:07:58 +00:00
$detector -> getClient (),
$detector -> getDevice ()
));
2023-08-29 09:40:30 +00:00
2024-03-06 18:07:58 +00:00
$record = $geodb -> get ( $log [ 'ip' ]);
2021-08-05 05:06:38 +00:00
2024-03-06 18:07:58 +00:00
if ( $record ) {
$output [ $i ][ 'countryCode' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), false ) ? \strtolower ( $record [ 'country' ][ 'iso_code' ]) : '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'countries.' . strtolower ( $record [ 'country' ][ 'iso_code' ]), $locale -> getText ( 'locale.country.unknown' ));
} else {
$output [ $i ][ 'countryCode' ] = '--' ;
$output [ $i ][ 'countryName' ] = $locale -> getText ( 'locale.country.unknown' );
2021-07-04 14:14:06 +00:00
}
2023-04-19 08:29:29 +00:00
}
2024-03-06 18:07:58 +00:00
$response -> dynamic ( new Document ([
2025-12-14 08:00:31 +00:00
'total' => $includeTotal ? $audit -> countLogsByUser ( $user -> getSequence ()) : 0 ,
2025-02-25 06:21:35 +00:00
'logs' => $output ,
2024-03-06 18:07:58 +00:00
]), Response :: MODEL_LOG_LIST );
2021-06-16 10:14:08 +00:00
});
2023-05-29 13:58:45 +00:00
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/name' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update name' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].update.name' )
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2022-09-08 13:06:16 +00:00
-> label ( 'audits.event' , 'user.update' )
2022-08-17 07:05:37 +00:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updateName' ,
description : '/docs/references/account/update-name.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2020-09-10 14:40:14 +00:00
-> param ( 'name' , '' , new Text ( 128 ), 'User name. Max length: 128 chars.' )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> action ( function ( string $name , Response $response , Document $user , Database $dbForProject , Event $queueForEvents ) {
2021-08-30 10:44:52 +00:00
2023-05-30 20:55:33 +00:00
$user -> setAttribute ( 'name' , $name );
2021-08-30 10:44:52 +00:00
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2021-08-30 10:44:52 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2021-08-16 08:53:34 +00:00
2022-05-06 08:58:36 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2021-08-30 10:44:52 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/password' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update password' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].update.password' )
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2022-09-08 13:06:16 +00:00
-> label ( 'audits.event' , 'user.update' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2022-08-16 14:56:05 +00:00
-> label ( 'audits.userId' , '{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updatePassword' ,
description : '/docs/references/account/update-password.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2024-06-10 06:09:30 +00:00
-> label ( 'abuse-limit' , 10 )
2023-03-09 01:21:24 +00:00
-> param ( 'password' , '' , fn ( $project , $passwordsDictionary ) => new PasswordDictionary ( $passwordsDictionary , $project -> getAttribute ( 'auths' , [])[ 'passwordDictionary' ] ? ? false ), 'New user password. Must be at least 8 chars.' , false , [ 'project' , 'passwordsDictionary' ])
2021-12-10 12:27:11 +00:00
-> param ( 'oldPassword' , '' , new Password (), 'Current user password. Must be at least 8 chars.' , true )
2021-08-30 10:44:52 +00:00
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2022-10-31 14:54:15 +00:00
-> inject ( 'project' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-01-04 15:26:15 +00:00
-> inject ( 'hooks' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
-> action ( function ( string $password , string $oldPassword , Response $response , User $user , Document $project , Database $dbForProject , Event $queueForEvents , Hooks $hooks , Store $store , ProofsPassword $proofForPassword , ProofsToken $proofForToken ) {
$userProofForPassword = ProofsPassword :: createHash ( $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ));
2021-05-04 18:52:46 +00:00
// Check old password only if its an existing user.
2025-11-04 03:48:57 +00:00
if ( ! empty ( $user -> getAttribute ( 'passwordUpdate' )) && ! $userProofForPassword -> verify ( $oldPassword , $user -> getAttribute ( 'password' ))) { // Double check user password
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-29 21:43:34 +00:00
}
2022-06-08 09:20:23 +00:00
2025-11-04 03:48:57 +00:00
$newPassword = $proofForPassword -> hash ( $password );
2022-12-18 06:27:41 +00:00
$historyLimit = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
2025-11-04 03:48:57 +00:00
$hash = ProofsPassword :: createHash ( $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ));
2023-07-19 21:34:27 +00:00
$history = $user -> getAttribute ( 'passwordHistory' , []);
2025-11-04 03:48:57 +00:00
2022-12-18 06:31:14 +00:00
if ( $historyLimit > 0 ) {
2025-11-04 03:48:57 +00:00
$validator = new PasswordHistory ( $history , $hash );
2022-12-18 09:08:51 +00:00
if ( ! $validator -> isValid ( $password )) {
2023-04-13 20:20:03 +00:00
throw new Exception ( Exception :: USER_PASSWORD_RECENTLY_USED );
2022-12-16 10:47:08 +00:00
}
2021-08-30 10:44:52 +00:00
2022-12-16 10:47:08 +00:00
$history [] = $newPassword ;
2023-07-19 21:34:27 +00:00
$history = array_slice ( $history , ( count ( $history ) - $historyLimit ), $historyLimit );
2021-08-30 10:44:52 +00:00
}
2023-07-19 22:24:32 +00:00
if ( $project -> getAttribute ( 'auths' , [])[ 'personalDataCheck' ] ? ? false ) {
2023-04-13 20:20:03 +00:00
$personalDataValidator = new PersonalData ( $user -> getId (), $user -> getAttribute ( 'email' ), $user -> getAttribute ( 'name' ), $user -> getAttribute ( 'phone' ));
if ( ! $personalDataValidator -> isValid ( $password )) {
throw new Exception ( Exception :: USER_PASSWORD_PERSONAL_DATA );
}
2021-08-30 10:44:52 +00:00
}
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , true ]);
2023-07-07 00:12:39 +00:00
2023-01-20 00:36:17 +00:00
$user
-> setAttribute ( 'password' , $newPassword )
-> setAttribute ( 'passwordHistory' , $history )
-> setAttribute ( 'passwordUpdate' , DateTime :: now ())
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'hash' , $proofForPassword -> getHash () -> getName ())
-> setAttribute ( 'hashOptions' , $proofForPassword -> getHash () -> getOptions ());
2022-07-05 10:59:03 +00:00
2025-06-18 08:00:24 +00:00
$sessions = $user -> getAttribute ( 'sessions' , []);
2025-11-04 03:48:57 +00:00
$current = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2025-06-18 20:56:58 +00:00
$invalidate = $project -> getAttribute ( 'auths' , default : [])[ 'invalidateSessions' ] ? ? false ;
if ( $invalidate && ! empty ( $current )) {
foreach ( $sessions as $session ) {
/** @var Document $session */
if ( $session -> getId () !== $current ) {
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
}
2025-06-18 08:00:24 +00:00
}
}
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2021-08-30 10:44:52 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2021-08-30 10:44:52 +00:00
2022-05-06 08:58:36 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2020-12-26 14:31:53 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/email' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update email' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].update.email' )
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2022-09-08 13:06:16 +00:00
-> label ( 'audits.event' , 'user.update' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updateEmail' ,
description : '/docs/references/account/update-email.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2021-11-25 20:07:54 +00:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2023-01-20 00:36:17 +00:00
-> inject ( 'requestTimestamp' )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-01-05 11:31:38 +00:00
-> inject ( 'project' )
-> inject ( 'hooks' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForPassword' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $email , string $password , ? \DateTime $requestTimestamp , Response $response , User $user , Database $dbForProject , Event $queueForEvents , Document $project , Hooks $hooks , ProofsPassword $proofForPassword , Authorization $authorization ) {
2023-05-25 21:16:19 +00:00
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user -> getAttribute ( 'passwordUpdate' );
2021-08-30 10:44:52 +00:00
2025-11-04 03:48:57 +00:00
$userProofForPassword = ProofsPassword :: createHash ( $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ));
2021-02-16 13:46:30 +00:00
if (
2023-05-25 21:16:19 +00:00
! empty ( $passwordUpdate ) &&
2025-11-04 03:48:57 +00:00
! $userProofForPassword -> verify ( $password , $user -> getAttribute ( 'password' ))
2021-02-16 13:46:30 +00:00
) { // Double check user password
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2020-06-29 21:43:34 +00:00
}
2022-04-04 09:59:32 +00:00
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , false ]);
2021-10-07 19:10:43 +00:00
2023-11-14 17:16:20 +00:00
$oldEmail = $user -> getAttribute ( 'email' );
2021-08-30 10:44:52 +00:00
2021-06-03 13:03:51 +00:00
$email = \strtolower ( $email );
2021-08-30 10:44:52 +00:00
2023-05-18 01:11:45 +00:00
// Makes sure this email is not already used in another identity
$identityWithMatchingEmail = $dbForProject -> findOne ( 'identities' , [
Query :: equal ( 'providerEmail' , [ $email ]),
2025-05-26 05:42:11 +00:00
Query :: notEqual ( 'userInternalId' , $user -> getSequence ()),
2023-05-18 01:11:45 +00:00
]);
2024-10-31 08:13:23 +00:00
if ( ! $identityWithMatchingEmail -> isEmpty ()) {
2024-03-01 07:37:31 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST ); /** Return a generic bad request to prevent exposing existing accounts */
2021-08-30 10:44:52 +00:00
}
2021-09-01 07:29:12 +00:00
2025-11-11 08:35:39 +00:00
try {
2025-11-11 13:25:10 +00:00
$emailCanonical = new Email ( $email );
2025-11-11 08:35:39 +00:00
} catch ( Throwable ) {
2025-11-11 13:25:10 +00:00
$emailCanonical = null ;
2025-11-11 08:35:39 +00:00
}
2025-10-30 16:09:36 +00:00
2022-06-08 12:50:31 +00:00
$user
-> setAttribute ( 'email' , $email )
-> setAttribute ( 'emailVerification' , false ) // After this user needs to confirm mail again
2025-11-11 13:25:10 +00:00
-> setAttribute ( 'emailCanonical' , $emailCanonical ? -> getCanonical ())
-> setAttribute ( 'emailIsCanonical' , $emailCanonical ? -> isCanonicalSupported ())
-> setAttribute ( 'emailIsCorporate' , $emailCanonical ? -> isCorporate ())
-> setAttribute ( 'emailIsDisposable' , $emailCanonical ? -> isDisposable ())
-> setAttribute ( 'emailIsFree' , $emailCanonical ? -> isFree ())
2023-05-30 20:55:33 +00:00
;
2021-08-30 10:44:52 +00:00
2023-05-25 21:16:19 +00:00
if ( empty ( $passwordUpdate )) {
$user
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'password' , $proofForPassword -> hash ( $password ))
-> setAttribute ( 'hash' , $proofForPassword -> getHash () -> getName ())
-> setAttribute ( 'hashOptions' , $proofForPassword -> getHash () -> getOptions ())
2023-05-25 21:16:19 +00:00
-> setAttribute ( 'passwordUpdate' , DateTime :: now ());
2021-08-30 10:44:52 +00:00
}
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> findOne ( 'targets' , [
2023-11-14 17:16:20 +00:00
Query :: equal ( 'identifier' , [ $email ]),
2023-11-28 13:12:34 +00:00
]));
2021-08-30 10:44:52 +00:00
2024-10-31 08:13:23 +00:00
if ( ! $target -> isEmpty ()) {
2023-11-14 17:16:20 +00:00
throw new Exception ( Exception :: USER_TARGET_ALREADY_EXISTS );
}
2021-08-29 12:00:25 +00:00
try {
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2023-11-28 13:12:34 +00:00
/**
* @ var Document $oldTarget
*/
$oldTarget = $user -> find ( 'identifier' , $oldEmail , 'targets' );
2021-08-30 10:44:52 +00:00
2023-11-28 13:12:34 +00:00
if ( $oldTarget instanceof Document && ! $oldTarget -> isEmpty ()) {
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'targets' , $oldTarget -> getId (), $oldTarget -> setAttribute ( 'identifier' , $email )));
2023-11-28 13:12:34 +00:00
}
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2023-06-16 02:16:19 +00:00
} catch ( Duplicate ) {
2024-03-01 07:37:31 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST ); /** Return a generic bad request to prevent exposing existing accounts */
2021-08-29 12:00:25 +00:00
}
2021-08-30 10:44:52 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2021-08-30 10:44:52 +00:00
2022-05-06 08:58:36 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2021-08-30 10:44:52 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/phone' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Update phone' )
2022-06-08 12:50:31 +00:00
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].update.phone' )
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2022-09-08 13:06:16 +00:00
-> label ( 'audits.event' , 'user.update' )
2022-08-08 14:32:54 +00:00
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updatePhone' ,
description : '/docs/references/account/update-phone.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2022-08-14 15:10:12 +00:00
-> param ( 'phone' , '' , new Phone (), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.' )
2022-06-08 12:50:31 +00:00
-> param ( 'password' , '' , new Password (), 'User password. Must be at least 8 chars.' )
2022-06-08 09:00:38 +00:00
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2022-06-08 09:00:38 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-01-05 11:31:38 +00:00
-> inject ( 'project' )
-> inject ( 'hooks' )
2026-01-14 15:08:00 +00:00
-> inject ( 'proofForPassword' )
-> inject ( 'authorization' )
-> action ( function ( string $phone , string $password , Response $response , Document $user , Database $dbForProject , Event $queueForEvents , Document $project , Hooks $hooks , ProofsPassword $proofForPassword , Authorization $authorization ) {
2023-05-25 21:16:19 +00:00
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user -> getAttribute ( 'passwordUpdate' );
2022-08-17 07:32:42 +00:00
2025-11-04 03:48:57 +00:00
$userProofForPassword = ProofsPassword :: createHash ( $user -> getAttribute ( 'hash' ), $user -> getAttribute ( 'hashOptions' ));
2022-06-08 12:50:31 +00:00
if (
2023-05-25 21:16:19 +00:00
! empty ( $passwordUpdate ) &&
2025-11-04 03:48:57 +00:00
! $userProofForPassword -> verify ( $password , $user -> getAttribute ( 'password' ))
2022-06-08 12:50:31 +00:00
) { // Double check user password
2022-08-16 06:59:03 +00:00
throw new Exception ( Exception :: USER_INVALID_CREDENTIALS );
2022-06-08 09:00:38 +00:00
}
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , false ]);
2022-06-08 09:00:38 +00:00
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> findOne ( 'targets' , [
2023-11-14 17:16:20 +00:00
Query :: equal ( 'identifier' , [ $phone ]),
2023-11-28 13:12:34 +00:00
]));
2022-06-08 09:00:38 +00:00
2024-10-31 08:13:23 +00:00
if ( ! $target -> isEmpty ()) {
2023-11-14 17:16:20 +00:00
throw new Exception ( Exception :: USER_TARGET_ALREADY_EXISTS );
}
2022-06-08 09:00:38 +00:00
2023-11-28 13:12:34 +00:00
$oldPhone = $user -> getAttribute ( 'phone' );
2022-06-08 09:00:38 +00:00
2022-06-08 12:50:31 +00:00
$user
-> setAttribute ( 'phone' , $phone )
-> setAttribute ( 'phoneVerification' , false ) // After this user needs to confirm phone number again
2023-05-30 20:55:33 +00:00
;
2023-07-07 00:12:39 +00:00
2023-05-31 20:52:05 +00:00
if ( empty ( $passwordUpdate )) {
$user
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'password' , $proofForPassword -> hash ( $password ))
-> setAttribute ( 'hash' , $proofForPassword -> getHash () -> getName ())
-> setAttribute ( 'hashOptions' , $proofForPassword -> getHash () -> getOptions ())
2023-05-31 20:52:05 +00:00
-> setAttribute ( 'passwordUpdate' , DateTime :: now ());
2022-06-08 09:00:38 +00:00
}
2022-06-08 12:50:31 +00:00
try {
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2023-11-28 13:12:34 +00:00
/**
2024-03-06 18:07:58 +00:00
* @ var Document $oldTarget
*/
$oldTarget = $user -> find ( 'identifier' , $oldPhone , 'targets' );
2022-11-03 15:03:39 +00:00
2024-03-06 18:07:58 +00:00
if ( $oldTarget instanceof Document && ! $oldTarget -> isEmpty ()) {
2026-01-14 15:08:00 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'targets' , $oldTarget -> getId (), $oldTarget -> setAttribute ( 'identifier' , $phone )));
2024-02-11 14:51:19 +00:00
}
2024-03-06 18:07:58 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
} catch ( Duplicate $th ) {
throw new Exception ( Exception :: USER_PHONE_ALREADY_EXISTS );
2024-02-11 14:51:19 +00:00
}
2024-02-11 14:58:05 +00:00
2024-03-06 18:07:58 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2022-06-08 09:00:38 +00:00
2024-03-06 18:07:58 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2022-02-01 15:54:20 +00:00
});
2022-06-08 09:00:38 +00:00
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/prefs' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Update preferences' )
2020-06-29 21:43:34 +00:00
-> groups ([ 'api' , 'account' ])
2024-03-06 18:07:58 +00:00
-> label ( 'event' , 'users.[userId].update.prefs' )
2024-02-20 11:45:11 +00:00
-> label ( 'scope' , 'account' )
2024-03-06 18:07:58 +00:00
-> label ( 'audits.event' , 'user.update' )
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updatePrefs' ,
description : '/docs/references/account/update-prefs.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON
))
2025-09-04 04:52:51 +00:00
-> param ( 'prefs' , [], new Assoc (), 'Prefs key-value JSON object.' , example : '{"language":"en","timezone":"UTC","darkTheme":true}' )
2024-03-06 18:07:58 +00:00
-> inject ( 'requestTimestamp' )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-03-06 18:07:58 +00:00
-> action ( function ( array $prefs , ? \DateTime $requestTimestamp , Response $response , Document $user , Database $dbForProject , Event $queueForEvents ) {
2022-06-08 09:00:38 +00:00
2024-03-06 18:07:58 +00:00
$user -> setAttribute ( 'prefs' , $prefs );
2022-06-08 09:00:38 +00:00
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-06-08 09:00:38 +00:00
2024-03-06 18:07:58 +00:00
$queueForEvents -> setParam ( 'userId' , $user -> getId ());
2023-04-19 08:44:22 +00:00
2024-03-06 18:07:58 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
});
2023-04-19 08:29:29 +00:00
2026-02-04 05:30:22 +00:00
Http :: patch ( '/v1/account/status' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Update status' )
-> groups ([ 'api' , 'account' ])
-> label ( 'event' , 'users.[userId].update.status' )
-> label ( 'scope' , 'account' )
-> label ( 'audits.event' , 'user.update' )
-> label ( 'audits.resource' , 'user/{response.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'account' ,
2025-01-17 04:31:39 +00:00
name : 'updateStatus' ,
description : '/docs/references/account/update-status.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_USER ,
)
],
contentType : ContentType :: JSON ,
))
2024-03-06 18:07:58 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> action ( function ( Request $request , Response $response , Document $user , Database $dbForProject , Event $queueForEvents , Store $store ) {
2023-03-14 09:07:42 +00:00
2024-03-06 18:07:58 +00:00
$user -> setAttribute ( 'status' , false );
2022-06-08 09:00:38 +00:00
2025-04-30 05:40:47 +00:00
$user = $dbForProject -> updateDocument ( 'users' , $user -> getId (), $user );
2022-06-08 09:00:38 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-04-13 12:39:31 +00:00
-> setParam ( 'userId' , $user -> getId ())
2024-03-06 18:07:58 +00:00
-> setPayload ( $response -> output ( $user , Response :: MODEL_ACCOUNT ));
2020-06-29 21:43:34 +00:00
2024-03-06 18:07:58 +00:00
if ( ! Config :: getParam ( 'domainVerification' )) {
$response -> addHeader ( 'X-Fallback-Cookies' , \json_encode ([]));
}
2022-06-08 09:00:38 +00:00
2024-03-06 18:07:58 +00:00
$protocol = $request -> getProtocol ();
2022-06-08 09:00:38 +00:00
$response
2025-11-04 03:48:57 +00:00
-> addCookie ( $store -> getKey () . '_legacy' , '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , null )
-> addCookie ( $store -> getKey (), '' , \time () - 3600 , '/' , Config :: getParam ( 'cookieDomain' ), ( 'https' == $protocol ), true , Config :: getParam ( 'cookieSamesite' ))
2022-06-08 09:00:38 +00:00
;
2024-03-06 18:07:58 +00:00
$response -> dynamic ( $user , Response :: MODEL_ACCOUNT );
2022-06-08 09:00:38 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/recovery' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Create password recovery' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].recovery.[tokenId].create' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'recovery.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'recovery' ,
2025-01-17 04:31:39 +00:00
name : 'createRecovery' ,
description : '/docs/references/account/create-recovery.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
))
2022-06-08 09:00:38 +00:00
-> label ( 'abuse-limit' , 10 )
2024-02-12 01:18:19 +00:00
-> label ( 'abuse-key' , [ 'url:{url},email:{param-email}' , 'url:{url},ip:{ip}' ])
2025-11-11 13:25:10 +00:00
-> param ( 'email' , '' , new EmailValidator (), 'User email.' )
2025-12-07 20:29:45 +00:00
-> param ( 'url' , '' , fn ( $redirectValidator ) => $redirectValidator , '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 , [ 'redirectValidator' ])
2022-06-08 09:00:38 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2022-06-08 09:00:38 +00:00
-> inject ( 'dbForProject' )
2022-10-31 14:54:15 +00:00
-> inject ( 'project' )
2025-12-17 11:35:08 +00:00
-> inject ( 'platform' )
2022-06-08 09:00:38 +00:00
-> inject ( 'locale' )
2023-06-11 14:08:48 +00:00
-> inject ( 'queueForMails' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $email , string $url , Request $request , Response $response , User $user , Database $dbForProject , Document $project , array $platform , Locale $locale , Mail $queueForMails , Event $queueForEvents , ProofsToken $proofForToken , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-13 15:15:19 +00:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP Disabled' );
2021-08-31 07:35:04 +00:00
}
2025-12-17 11:35:08 +00:00
2024-07-22 13:37:28 +00:00
$url = htmlentities ( $url );
2021-06-03 13:03:51 +00:00
$email = \strtolower ( $email );
2022-06-08 09:00:38 +00:00
2022-05-12 16:25:36 +00:00
$profile = $dbForProject -> findOne ( 'users' , [
2022-08-11 23:53:52 +00:00
Query :: equal ( 'email' , [ $email ]),
2022-05-12 16:25:36 +00:00
]);
2022-06-08 09:00:38 +00:00
2024-10-07 02:40:01 +00:00
if ( $profile -> isEmpty ()) {
2022-08-15 07:54:54 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2022-06-08 09:00:38 +00:00
}
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2022-06-08 09:00:38 +00:00
2021-07-14 11:02:12 +00:00
if ( false === $profile -> getAttribute ( 'status' )) { // Account is blocked
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_BLOCKED );
2022-06-08 09:00:38 +00:00
}
2025-11-04 03:48:57 +00:00
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_RECOVERY ));
2022-07-05 10:59:03 +00:00
2025-11-04 03:48:57 +00:00
$secret = $proofForToken -> generate ();
2020-06-29 21:43:34 +00:00
$recovery = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2021-06-12 20:44:25 +00:00
'userId' => $profile -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $profile -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_RECOVERY ,
'secret' => $proofForToken -> hash ( $secret ), // One way hash encryption to protect DB leak
2021-07-06 12:18:55 +00:00
'expire' => $expire ,
2020-07-03 15:14:51 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-29 21:43:34 +00:00
'ip' => $request -> getIP (),
]);
2022-06-08 09:00:38 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $profile -> getId ()) -> toString ());
2022-06-08 09:00:38 +00:00
2022-04-27 11:06:53 +00:00
$recovery = $dbForProject -> createDocument ( 'tokens' , $recovery
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $profile -> getId ())),
Permission :: update ( Role :: user ( $profile -> getId ())),
Permission :: delete ( Role :: user ( $profile -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2022-06-08 09:00:38 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2022-06-08 09:00:38 +00:00
2020-06-29 21:43:34 +00:00
$url = Template :: parseURL ( $url );
2021-07-06 12:18:55 +00:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $profile -> getId (), 'secret' => $secret , 'expire' => $expire ]);
2020-06-29 21:43:34 +00:00
$url = Template :: unParseURL ( $url );
2022-06-08 09:00:38 +00:00
2025-12-17 11:35:08 +00:00
$projectName = $project -> isEmpty ()
? 'Console'
: $project -> getAttribute ( 'name' , '[APP-NAME]' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2023-08-27 22:45:37 +00:00
$body = $locale -> getText ( " emails.recovery.body " );
2022-12-15 09:22:05 +00:00
$subject = $locale -> getText ( " emails.recovery.subject " );
2025-07-23 16:34:25 +00:00
$preview = $locale -> getText ( " emails.recovery.preview " );
2023-04-19 08:29:29 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.recovery-' . $locale -> default ] ? ? [];
2022-06-08 09:00:38 +00:00
2023-08-29 09:40:30 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-inner-base.tpl' );
2023-10-04 23:14:27 +00:00
$message
2024-01-08 17:08:17 +00:00
-> setParam ( '{{body}}' , $body , escapeHtml : false )
2023-10-04 23:14:27 +00:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.recovery.hello " ))
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.recovery.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.recovery.thanks " ))
2025-03-28 11:14:44 +00:00
-> setParam ( '{{buttonText}}' , $locale -> getText ( " emails.recovery.buttonText " ))
2023-10-04 23:14:27 +00:00
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.recovery.signature " ));
2023-08-29 09:40:30 +00:00
$body = $message -> render ();
2022-06-08 09:00:38 +00:00
2023-08-29 09:40:30 +00:00
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
2022-06-08 09:00:38 +00:00
2024-04-01 11:02:47 +00:00
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
2023-08-29 09:40:30 +00:00
$replyTo = " " ;
2022-06-08 09:00:38 +00:00
2023-08-29 09:40:30 +00:00
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
}
2023-08-28 12:19:37 +00:00
2023-09-27 15:51:17 +00:00
$queueForMails
2023-08-25 15:13:25 +00:00
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
2023-08-29 09:40:30 +00:00
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
2023-08-28 12:19:37 +00:00
2023-08-30 04:30:44 +00:00
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
}
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
2023-08-29 09:40:30 +00:00
}
2023-09-27 15:51:17 +00:00
$queueForMails
2023-08-30 04:30:44 +00:00
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
2022-06-08 09:00:38 +00:00
}
2023-08-27 22:45:37 +00:00
$emailVariables = [
'direction' => $locale -> getText ( 'settings.direction' ),
2024-02-23 00:46:13 +00:00
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
2023-08-30 21:54:26 +00:00
'user' => $profile -> getAttribute ( 'name' ),
2024-01-11 20:36:05 +00:00
'redirect' => $url ,
2024-02-23 00:46:13 +00:00
'project' => $projectName ,
// TODO: remove unnecessary team variable from this email
'team' => ''
2023-08-27 22:45:37 +00:00
];
2022-06-08 09:00:38 +00:00
2023-06-11 14:08:48 +00:00
$queueForMails
2022-04-13 12:39:31 +00:00
-> setRecipient ( $profile -> getAttribute ( 'email' , '' ))
2024-02-24 13:21:00 +00:00
-> setName ( $profile -> getAttribute ( 'name' , '' ))
2022-12-15 09:22:05 +00:00
-> setBody ( $body )
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2022-12-15 09:22:05 +00:00
-> setSubject ( $subject )
2025-12-17 11:35:08 +00:00
-> setPreview ( $preview );
if ( $project -> getId () === 'console' ) {
$queueForMails -> setSenderName ( $platform [ 'emailSenderName' ]);
}
$queueForMails -> trigger ();
2022-06-08 09:00:38 +00:00
2024-05-22 02:11:06 +00:00
$recovery -> setAttribute ( 'secret' , $secret );
2022-06-08 09:00:38 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-04-13 12:39:31 +00:00
-> setParam ( 'userId' , $profile -> getId ())
-> setParam ( 'tokenId' , $recovery -> getId ())
2022-04-18 16:21:45 +00:00
-> setUser ( $profile )
2025-03-26 14:37:35 +00:00
-> setPayload ( Response :: showSensitive ( fn () => $response -> output ( $recovery , Response :: MODEL_TOKEN )), sensitive : [ 'secret' ]);
2020-11-18 19:38:31 +00:00
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $recovery , Response :: MODEL_TOKEN );
2022-06-08 09:00:38 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/recovery' )
2025-04-14 18:40:48 +00:00
-> desc ( 'Update password recovery (confirmation)' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-01-17 11:17:03 +00:00
-> label ( 'scope' , 'sessions.write' )
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].recovery.[tokenId].update' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'recovery.update' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2022-08-16 09:00:28 +00:00
-> label ( 'audits.userId' , '{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'recovery' ,
2025-01-17 04:31:39 +00:00
name : 'updateRecovery' ,
description : '/docs/references/account/update-recovery.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON
))
2020-01-05 23:07:41 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2021-12-10 12:27:11 +00:00
-> param ( 'userId' , '' , new UID (), 'User ID.' )
2020-09-10 14:40:14 +00:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid reset token.' )
2024-01-02 10:59:35 +00:00
-> param ( 'password' , '' , fn ( $project , $passwordsDictionary ) => new PasswordDictionary ( $passwordsDictionary , $project -> getAttribute ( 'auths' , [])[ 'passwordDictionary' ] ? ? false ), 'New user password. Must be between 8 and 256 chars.' , false , [ 'project' , 'passwordsDictionary' ])
2021-02-16 13:46:30 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2023-07-19 21:34:27 +00:00
-> inject ( 'project' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2024-01-05 11:31:38 +00:00
-> inject ( 'hooks' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForPassword' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $secret , string $password , Response $response , User $user , Database $dbForProject , Document $project , Event $queueForEvents , Hooks $hooks , ProofsPassword $proofForPassword , ProofsToken $proofForToken , Authorization $authorization ) {
2025-11-04 03:48:57 +00:00
/** @var Appwrite\Utopia\Database\Documents\User $profile */
2021-12-27 12:45:23 +00:00
$profile = $dbForProject -> getDocument ( 'users' , $userId );
2021-04-03 08:56:32 +00:00
2022-05-16 09:58:17 +00:00
if ( $profile -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2021-04-03 08:56:32 +00:00
}
2025-11-04 03:48:57 +00:00
$verifiedToken = $profile -> tokenVerify ( TOKEN_TYPE_RECOVERY , $secret , $proofForToken );
2021-02-16 13:46:30 +00:00
2023-10-05 10:18:19 +00:00
if ( ! $verifiedToken ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2020-06-29 21:43:34 +00:00
}
2021-07-17 10:04:43 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $profile -> getId ()) -> toString ());
2021-02-16 13:46:30 +00:00
2025-11-04 03:48:57 +00:00
$newPassword = $proofForPassword -> hash ( $password );
2022-04-04 06:30:07 +00:00
2025-11-04 03:48:57 +00:00
$hash = ProofsPassword :: createHash ( $profile -> getAttribute ( 'hash' ), $profile -> getAttribute ( 'hashOptions' ));
2023-07-19 21:34:27 +00:00
$historyLimit = $project -> getAttribute ( 'auths' , [])[ 'passwordHistory' ] ? ? 0 ;
$history = $profile -> getAttribute ( 'passwordHistory' , []);
2025-11-04 03:48:57 +00:00
2023-07-19 21:34:27 +00:00
if ( $historyLimit > 0 ) {
2025-11-04 03:48:57 +00:00
$validator = new PasswordHistory ( $history , $hash );
2023-07-19 21:34:27 +00:00
if ( ! $validator -> isValid ( $password )) {
throw new Exception ( Exception :: USER_PASSWORD_RECENTLY_USED );
}
$history [] = $newPassword ;
$history = array_slice ( $history , ( count ( $history ) - $historyLimit ), $historyLimit );
2021-02-16 13:46:30 +00:00
}
2024-01-05 11:31:38 +00:00
$hooks -> trigger ( 'passwordValidator' , [ $dbForProject , $project , $password , & $user , true ]);
2021-02-16 13:46:30 +00:00
2021-12-27 12:45:23 +00:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile
2025-11-04 03:48:57 +00:00
-> setAttribute ( 'password' , $newPassword )
-> setAttribute ( 'passwordHistory' , $history )
-> setAttribute ( 'passwordUpdate' , DateTime :: now ())
-> setAttribute ( 'hash' , $proofForPassword -> getHash () -> getName ())
-> setAttribute ( 'hashOptions' , $proofForPassword -> getHash () -> getOptions ())
-> setAttribute ( 'emailVerification' , true ));
2021-06-17 09:33:57 +00:00
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2023-10-05 10:18:19 +00:00
$recoveryDocument = $dbForProject -> getDocument ( 'tokens' , $verifiedToken -> getId ());
2022-04-27 11:06:53 +00:00
2020-06-29 21:43:34 +00:00
/**
2021-08-14 18:56:28 +00:00
* We act like we ' re updating and validating
* the recovery token but actually we don ' t need it anymore .
*/
2023-10-05 10:18:19 +00:00
$dbForProject -> deleteDocument ( 'tokens' , $verifiedToken -> getId ());
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2021-05-06 22:31:05 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2020-06-29 21:43:34 +00:00
-> setParam ( 'userId' , $profile -> getId ())
2022-05-08 15:49:17 +00:00
-> setParam ( 'tokenId' , $recoveryDocument -> getId ())
2025-03-26 14:37:35 +00:00
-> setPayload ( Response :: showSensitive ( fn () => $response -> output ( $recoveryDocument , Response :: MODEL_TOKEN )), sensitive : [ 'secret' ]);
2021-02-16 13:46:30 +00:00
2022-04-27 11:06:53 +00:00
$response -> dynamic ( $recoveryDocument , Response :: MODEL_TOKEN );
2021-02-16 13:46:30 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/verifications/email' )
2025-10-06 13:43:06 +00:00
-> alias ( '/v1/account/verification' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Create email verification' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2020-10-17 17:49:09 +00:00
-> label ( 'scope' , 'account' )
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].verification.[tokenId].create' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'verification.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2025-10-06 11:14:31 +00:00
-> label ( 'sdk' , [
new Method (
namespace : 'account' ,
group : 'verification' ,
name : 'createEmailVerification' ,
description : '/docs/references/account/create-email-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-10-06 11:14:31 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
),
new Method (
namespace : 'account' ,
group : 'verification' ,
name : 'createVerification' ,
description : '/docs/references/account/create-email-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-10-06 11:14:31 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
deprecated : new Deprecated (
since : '1.8.0' ,
replaceWith : 'account.createEmailVerification'
),
2025-12-11 08:23:34 +00:00
public : false ,
2025-10-06 11:14:31 +00:00
)
])
2020-01-12 00:20:35 +00:00
-> label ( 'abuse-limit' , 10 )
2021-07-27 11:07:39 +00:00
-> label ( 'abuse-key' , 'url:{url},userId:{userId}' )
2025-12-07 20:29:45 +00:00
-> param ( 'url' , '' , fn ( $redirectValidator ) => $redirectValidator , '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 , [ 'redirectValidator' ]) // TODO add built-in confirm page
2020-12-26 14:31:53 +00:00
-> inject ( 'request' )
2020-12-28 17:03:27 +00:00
-> inject ( 'response' )
2020-12-26 14:31:53 +00:00
-> inject ( 'project' )
2025-12-17 11:35:08 +00:00
-> inject ( 'platform' )
2020-12-28 17:03:27 +00:00
-> inject ( 'user' )
2022-04-04 09:59:32 +00:00
-> inject ( 'dbForProject' )
2020-12-26 14:31:53 +00:00
-> inject ( 'locale' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2023-06-11 14:08:48 +00:00
-> inject ( 'queueForMails' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $url , Request $request , Response $response , Document $project , array $platform , User $user , Database $dbForProject , Locale $locale , Event $queueForEvents , Mail $queueForMails , ProofsToken $proofForToken , Authorization $authorization ) {
2022-04-26 08:52:59 +00:00
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMTP_HOST' ))) {
2022-08-13 15:15:19 +00:00
throw new Exception ( Exception :: GENERAL_SMTP_DISABLED , 'SMTP Disabled' );
2022-04-26 08:52:59 +00:00
}
2025-09-22 08:56:23 +00:00
if ( empty ( $user -> getAttribute ( 'email' ))) {
throw new Exception ( Exception :: USER_EMAIL_NOT_FOUND );
}
2024-07-22 13:37:28 +00:00
$url = htmlentities ( $url );
2023-09-22 17:26:07 +00:00
if ( $user -> getAttribute ( 'emailVerification' )) {
2023-09-22 17:23:41 +00:00
throw new Exception ( Exception :: USER_EMAIL_ALREADY_VERIFIED );
2020-12-28 17:03:27 +00:00
}
2021-08-05 05:06:38 +00:00
2025-11-04 03:48:57 +00:00
$verificationSecret = $proofForToken -> generate ();
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_CONFIRM ));
2020-12-28 17:03:27 +00:00
2020-06-29 21:43:34 +00:00
$verification = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2021-05-06 22:31:05 +00:00
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_VERIFICATION ,
'secret' => $proofForToken -> hash ( $verificationSecret ), // One way hash encryption to protect DB leak
2021-07-06 12:18:55 +00:00
'expire' => $expire ,
2020-07-03 15:14:51 +00:00
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
2020-06-29 21:43:34 +00:00
'ip' => $request -> getIP (),
]);
2020-01-31 22:34:07 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2020-01-31 22:34:07 +00:00
2022-04-27 11:06:53 +00:00
$verification = $dbForProject -> createDocument ( 'tokens' , $verification
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2020-06-29 21:43:34 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-01-31 22:34:07 +00:00
2020-06-29 21:43:34 +00:00
$url = Template :: parseURL ( $url );
2021-07-06 12:18:55 +00:00
$url [ 'query' ] = Template :: mergeQuery ((( isset ( $url [ 'query' ])) ? $url [ 'query' ] : '' ), [ 'userId' => $user -> getId (), 'secret' => $verificationSecret , 'expire' => $expire ]);
2020-06-29 21:43:34 +00:00
$url = Template :: unParseURL ( $url );
2025-12-17 11:35:08 +00:00
$projectName = $project -> isEmpty ()
? 'Console'
: $project -> getAttribute ( 'name' , '[APP-NAME]' );
if ( $project -> getId () === 'console' ) {
$projectName = $platform [ 'platformName' ];
}
2023-08-28 05:09:28 +00:00
$body = $locale -> getText ( " emails.verification.body " );
2025-07-23 16:34:25 +00:00
$preview = $locale -> getText ( " emails.verification.preview " );
2022-12-15 09:22:05 +00:00
$subject = $locale -> getText ( " emails.verification.subject " );
2025-10-06 14:28:01 +00:00
$heading = $locale -> getText ( " emails.verification.heading " );
2023-04-19 08:29:29 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'email.verification-' . $locale -> default ] ? ? [];
2025-10-06 14:28:01 +00:00
$smtpBaseTemplate = $project -> getAttribute ( 'smtpBaseTemplate' , 'email-base' );
2025-10-06 16:22:18 +00:00
2025-10-07 10:37:47 +00:00
$validator = new FileName ();
if ( ! $validator -> isValid ( $smtpBaseTemplate )) {
2025-10-06 16:22:18 +00:00
throw new Exception ( Exception :: GENERAL_BAD_REQUEST , 'Invalid template path' );
}
2025-10-06 14:28:01 +00:00
$bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl' ;
2020-06-29 21:43:34 +00:00
2023-08-29 09:40:30 +00:00
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/email-inner-base.tpl' );
2023-10-04 23:14:27 +00:00
$message
2024-01-08 17:08:17 +00:00
-> setParam ( '{{body}}' , $body , escapeHtml : false )
2023-10-04 23:14:27 +00:00
-> setParam ( '{{hello}}' , $locale -> getText ( " emails.verification.hello " ))
-> setParam ( '{{footer}}' , $locale -> getText ( " emails.verification.footer " ))
-> setParam ( '{{thanks}}' , $locale -> getText ( " emails.verification.thanks " ))
2025-03-28 11:14:44 +00:00
-> setParam ( '{{buttonText}}' , $locale -> getText ( " emails.verification.buttonText " ))
2023-10-04 23:14:27 +00:00
-> setParam ( '{{signature}}' , $locale -> getText ( " emails.verification.signature " ));
2021-06-17 09:08:01 +00:00
2023-08-29 09:40:30 +00:00
$body = $message -> render ();
2020-01-31 22:34:07 +00:00
2023-08-29 09:40:30 +00:00
$smtp = $project -> getAttribute ( 'smtp' , []);
$smtpEnabled = $smtp [ 'enabled' ] ? ? false ;
2020-06-29 21:43:34 +00:00
2024-04-01 11:02:47 +00:00
$senderEmail = System :: getEnv ( '_APP_SYSTEM_EMAIL_ADDRESS' , APP_EMAIL_TEAM );
$senderName = System :: getEnv ( '_APP_SYSTEM_EMAIL_NAME' , APP_NAME . ' Server' );
2023-08-29 09:40:30 +00:00
$replyTo = " " ;
2020-01-31 22:34:07 +00:00
2023-08-29 09:40:30 +00:00
if ( $smtpEnabled ) {
if ( ! empty ( $smtp [ 'senderEmail' ])) {
$senderEmail = $smtp [ 'senderEmail' ];
}
if ( ! empty ( $smtp [ 'senderName' ])) {
$senderName = $smtp [ 'senderName' ];
}
if ( ! empty ( $smtp [ 'replyTo' ])) {
$replyTo = $smtp [ 'replyTo' ];
}
2020-06-29 21:43:34 +00:00
2023-09-27 15:51:17 +00:00
$queueForMails
2023-08-25 15:13:25 +00:00
-> setSmtpHost ( $smtp [ 'host' ] ? ? '' )
-> setSmtpPort ( $smtp [ 'port' ] ? ? '' )
-> setSmtpUsername ( $smtp [ 'username' ] ? ? '' )
-> setSmtpPassword ( $smtp [ 'password' ] ? ? '' )
2023-08-29 09:40:30 +00:00
-> setSmtpSecure ( $smtp [ 'secure' ] ? ? '' );
2022-08-23 13:10:27 +00:00
2023-08-30 04:30:44 +00:00
if ( ! empty ( $customTemplate )) {
if ( ! empty ( $customTemplate [ 'senderEmail' ])) {
$senderEmail = $customTemplate [ 'senderEmail' ];
}
if ( ! empty ( $customTemplate [ 'senderName' ])) {
$senderName = $customTemplate [ 'senderName' ];
}
if ( ! empty ( $customTemplate [ 'replyTo' ])) {
$replyTo = $customTemplate [ 'replyTo' ];
}
2022-04-04 06:30:07 +00:00
2023-08-30 04:30:44 +00:00
$body = $customTemplate [ 'message' ] ? ? '' ;
$subject = $customTemplate [ 'subject' ] ? ? $subject ;
2023-08-29 09:40:30 +00:00
}
2020-06-29 21:43:34 +00:00
2023-09-27 15:51:17 +00:00
$queueForMails
2023-08-30 04:30:44 +00:00
-> setSmtpReplyTo ( $replyTo )
-> setSmtpSenderEmail ( $senderEmail )
-> setSmtpSenderName ( $senderName );
2023-04-19 08:29:29 +00:00
}
2020-06-29 21:43:34 +00:00
2023-08-28 05:09:28 +00:00
$emailVariables = [
2025-10-06 16:22:18 +00:00
'heading' => $heading ,
2023-08-28 05:09:28 +00:00
'direction' => $locale -> getText ( 'settings.direction' ),
2024-02-23 00:46:13 +00:00
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
2023-08-30 21:52:55 +00:00
'user' => $user -> getAttribute ( 'name' ),
2024-01-11 20:36:05 +00:00
'redirect' => $url ,
2024-02-23 00:46:13 +00:00
'project' => $projectName ,
// TODO: remove unnecessary team variable from this email
'team' => '' ,
2023-08-28 05:09:28 +00:00
];
2020-06-29 21:43:34 +00:00
2025-10-06 16:22:18 +00:00
if ( $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ) {
2025-09-24 07:36:20 +00:00
$emailVariables = array_merge ( $emailVariables , [
2025-12-17 11:57:18 +00:00
'accentColor' => $platform [ 'accentColor' ],
'logoUrl' => $platform [ 'logoUrl' ],
'twitter' => $platform [ 'twitterUrl' ],
'discord' => $platform [ 'discordUrl' ],
'github' => $platform [ 'githubUrl' ],
'terms' => $platform [ 'termsUrl' ],
'privacy' => $platform [ 'privacyUrl' ],
'platform' => $platform [ 'platformName' ],
2025-09-24 07:36:20 +00:00
]);
}
2023-06-11 14:08:48 +00:00
$queueForMails
2022-12-14 06:35:04 +00:00
-> setSubject ( $subject )
2025-07-23 16:34:25 +00:00
-> setPreview ( $preview )
2022-12-14 06:35:04 +00:00
-> setBody ( $body )
2025-09-15 19:28:23 +00:00
-> setBodyTemplate ( $bodyTemplate )
2026-01-30 09:50:53 +00:00
-> appendVariables ( $emailVariables )
2022-04-13 12:39:31 +00:00
-> setRecipient ( $user -> getAttribute ( 'email' ))
2025-12-17 11:57:18 +00:00
-> setName ( $user -> getAttribute ( 'name' ) ? ? '' );
if ( $project -> getId () === 'console' ) {
$queueForMails -> setSenderName ( $platform [ 'emailSenderName' ]);
}
$queueForMails -> trigger ();
2020-06-29 21:43:34 +00:00
2024-05-22 02:11:06 +00:00
$verification -> setAttribute ( 'secret' , $verificationSecret );
2020-10-30 19:53:27 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-04-04 06:30:07 +00:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $verification -> getId ())
2025-03-26 14:37:35 +00:00
-> setPayload ( Response :: showSensitive ( fn () => $response -> output ( $verification , Response :: MODEL_TOKEN )), sensitive : [ 'secret' ]);
2020-06-29 21:43:34 +00:00
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $verification , Response :: MODEL_TOKEN );
2020-12-26 14:31:53 +00:00
});
2020-01-31 22:34:07 +00:00
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/verifications/email' )
2025-10-06 13:43:06 +00:00
-> alias ( '/v1/account/verification' )
2025-04-14 18:40:48 +00:00
-> desc ( 'Update email verification (confirmation)' )
2021-06-16 10:14:08 +00:00
-> groups ([ 'api' , 'account' ])
2020-01-12 00:20:35 +00:00
-> label ( 'scope' , 'public' )
2022-04-04 06:30:07 +00:00
-> label ( 'event' , 'users.[userId].verification.[tokenId].update' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'verification.update' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2025-10-06 11:14:31 +00:00
-> label ( 'sdk' , [
new Method (
namespace : 'account' ,
group : 'verification' ,
name : 'updateEmailVerification' ,
description : '/docs/references/account/update-email-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-10-06 11:14:31 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON
),
new Method (
namespace : 'account' ,
group : 'verification' ,
name : 'updateVerification' ,
description : '/docs/references/account/update-email-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-10-06 11:14:31 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
deprecated : new Deprecated (
since : '1.8.0' ,
replaceWith : 'account.updateEmailVerification'
),
2025-12-11 08:23:34 +00:00
public : false ,
2025-10-06 11:14:31 +00:00
)
])
2020-01-12 00:20:35 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'url:{url},userId:{param-userId}' )
2021-12-10 12:27:11 +00:00
-> param ( 'userId' , '' , new UID (), 'User ID.' )
2020-09-10 14:40:14 +00:00
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
2021-06-16 10:14:08 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $secret , Response $response , User $user , Database $dbForProject , Event $queueForEvents , ProofsToken $proofForToken , Authorization $authorization ) {
2025-11-04 03:48:57 +00:00
/** @var Appwrite\Utopia\Database\Documents\User $profile */
2026-01-14 15:08:00 +00:00
$profile = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2021-06-16 10:14:08 +00:00
2021-05-06 22:31:05 +00:00
if ( $profile -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2020-06-29 21:43:34 +00:00
}
2021-06-17 09:33:57 +00:00
2025-11-04 03:48:57 +00:00
$verifiedToken = $profile -> tokenVerify ( TOKEN_TYPE_VERIFICATION , $secret , $proofForToken );
2021-08-05 05:06:38 +00:00
2023-10-05 10:18:19 +00:00
if ( ! $verifiedToken ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2021-07-04 14:14:06 +00:00
}
2021-06-16 10:48:12 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $profile -> getId ()) -> toString ());
2021-06-16 10:14:08 +00:00
2021-12-27 12:45:23 +00:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile -> setAttribute ( 'emailVerification' , true ));
2020-06-29 21:43:34 +00:00
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2023-01-20 00:36:17 +00:00
2024-04-03 03:36:53 +00:00
$verification = $dbForProject -> getDocument ( 'tokens' , $verifiedToken -> getId ());
2020-06-29 21:43:34 +00:00
/**
2021-08-14 18:56:28 +00:00
* We act like we ' re updating and validating
* the verification token but actually we don ' t need it anymore .
*/
2023-10-05 10:18:19 +00:00
$dbForProject -> deleteDocument ( 'tokens' , $verifiedToken -> getId ());
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2021-08-16 08:53:34 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2023-03-10 18:53:49 +00:00
-> setParam ( 'userId' , $userId )
2025-03-24 14:06:36 +00:00
-> setParam ( 'tokenId' , $verification -> getId ())
2025-03-26 14:37:35 +00:00
-> setPayload ( Response :: showSensitive ( fn () => $response -> output ( $verification , Response :: MODEL_TOKEN )), sensitive : [ 'secret' ]);
2022-04-04 06:30:07 +00:00
2024-04-03 03:36:53 +00:00
$response -> dynamic ( $verification , Response :: MODEL_TOKEN );
2020-12-26 14:31:53 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/verifications/phone' )
2025-10-07 04:28:39 +00:00
-> alias ( '/v1/account/verification/phone' )
2023-08-01 15:26:48 +00:00
-> desc ( 'Create phone verification' )
2024-02-12 01:18:19 +00:00
-> groups ([ 'api' , 'account' , 'auth' ])
2019-05-09 06:54:39 +00:00
-> label ( 'scope' , 'account' )
2024-02-12 01:18:19 +00:00
-> label ( 'auth.type' , 'phone' )
2022-06-08 12:50:31 +00:00
-> label ( 'event' , 'users.[userId].verification.[tokenId].create' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'verification.create' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'verification' ,
2025-01-17 04:31:39 +00:00
name : 'createPhoneVerification' ,
description : '/docs/references/account/create-phone-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON ,
))
2022-06-08 12:50:31 +00:00
-> label ( 'abuse-limit' , 10 )
2024-02-12 01:18:19 +00:00
-> label ( 'abuse-key' , [ 'url:{url},userId:{userId}' , 'url:{url},ip:{ip}' ])
2022-06-08 12:50:31 +00:00
-> inject ( 'request' )
2020-12-26 14:31:53 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
-> inject ( 'queueForMessaging' )
2023-04-19 08:29:29 +00:00
-> inject ( 'project' )
-> inject ( 'locale' )
2025-01-08 11:31:01 +00:00
-> inject ( 'timelimit' )
2025-01-30 04:53:53 +00:00
-> inject ( 'queueForStatsUsage' )
2025-01-08 11:31:01 +00:00
-> inject ( 'plan' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( Request $request , Response $response , User $user , Database $dbForProject , Event $queueForEvents , Messaging $queueForMessaging , Document $project , Locale $locale , callable $timelimit , StatsUsage $queueForStatsUsage , array $plan , ProofsCode $proofForCode , Authorization $authorization ) {
2024-04-01 11:02:47 +00:00
if ( empty ( System :: getEnv ( '_APP_SMS_PROVIDER' ))) {
2023-09-05 17:10:48 +00:00
throw new Exception ( Exception :: GENERAL_PHONE_DISABLED , 'Phone provider not configured' );
2020-06-29 21:43:34 +00:00
}
2019-05-09 06:54:39 +00:00
2024-06-16 07:28:22 +00:00
$phone = $user -> getAttribute ( 'phone' );
2024-07-03 09:35:56 +00:00
if ( empty ( $phone )) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_PHONE_NOT_FOUND );
2022-12-16 10:22:39 +00:00
}
2023-09-22 17:26:07 +00:00
if ( $user -> getAttribute ( 'phoneVerification' )) {
2023-09-22 17:23:41 +00:00
throw new Exception ( Exception :: USER_PHONE_ALREADY_VERIFIED );
2022-12-16 10:22:39 +00:00
}
2024-06-16 07:28:22 +00:00
$secret = null ;
$sendSMS = true ;
$mockNumbers = $project -> getAttribute ( 'auths' , [])[ 'mockNumbers' ] ? ? [];
foreach ( $mockNumbers as $mockNumber ) {
if ( $mockNumber [ 'phone' ] === $phone ) {
$secret = $mockNumber [ 'otp' ];
$sendSMS = false ;
break ;
}
}
2022-04-04 06:30:07 +00:00
2025-11-04 03:48:57 +00:00
$secret ? ? = $proofForCode -> generate ();
$expire = DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), TOKEN_EXPIRATION_CONFIRM ));
2019-05-09 06:54:39 +00:00
2022-06-08 12:50:31 +00:00
$verification = new Document ([
2022-08-14 14:22:38 +00:00
'$id' => ID :: unique (),
2022-06-08 12:50:31 +00:00
'userId' => $user -> getId (),
2025-09-10 07:03:11 +00:00
'userInternalId' => $user -> getSequence (),
2025-11-04 03:48:57 +00:00
'type' => TOKEN_TYPE_PHONE ,
'secret' => $proofForCode -> hash ( $secret ),
2022-06-08 12:50:31 +00:00
'expire' => $expire ,
'userAgent' => $request -> getUserAgent ( 'UNKNOWN' ),
'ip' => $request -> getIP (),
]);
2021-02-16 13:46:30 +00:00
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
2022-06-08 12:50:31 +00:00
$verification = $dbForProject -> createDocument ( 'tokens' , $verification
2022-08-02 09:21:53 +00:00
-> setAttribute ( '$permissions' , [
2022-08-15 11:24:31 +00:00
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
2022-08-02 09:21:53 +00:00
]));
2022-06-08 12:50:31 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2022-06-08 12:50:31 +00:00
2024-06-16 07:28:22 +00:00
if ( $sendSMS ) {
$message = Template :: fromFile ( __DIR__ . '/../../config/locale/templates/sms-base.tpl' );
2023-04-19 08:29:29 +00:00
2024-06-16 07:28:22 +00:00
$customTemplate = $project -> getAttribute ( 'templates' , [])[ 'sms.verification-' . $locale -> default ] ? ? [];
if ( ! empty ( $customTemplate )) {
$message = $customTemplate [ 'message' ] ? ? $message ;
}
2024-06-20 15:01:20 +00:00
$messageContent = Template :: fromString ( $locale -> getText ( " sms.verification.body " ));
$messageContent
-> setParam ( '{{project}}' , $project -> getAttribute ( 'name' ))
-> setParam ( '{{secret}}' , $secret );
$messageContent = \strip_tags ( $messageContent -> render ());
$message = $message -> setParam ( '{{token}}' , $messageContent );
2024-06-16 07:28:22 +00:00
$message = $message -> render ();
2024-06-20 15:01:20 +00:00
$messageDoc = new Document ([
'$id' => $verification -> getId (),
'data' => [
'content' => $message ,
],
]);
2024-06-16 07:28:22 +00:00
$queueForMessaging
2024-06-20 15:01:20 +00:00
-> setType ( MESSAGE_SEND_TYPE_INTERNAL )
-> setMessage ( $messageDoc )
-> setRecipients ([ $user -> getAttribute ( 'phone' )])
-> setProviderType ( MESSAGE_TYPE_SMS );
2025-01-08 11:31:01 +00:00
2026-01-15 12:59:30 +00:00
$helper = PhoneNumberUtil :: getInstance ();
2026-01-15 13:19:34 +00:00
try {
$countryCode = $helper -> parse ( $phone ) -> getCountryCode ();
2025-01-08 11:31:01 +00:00
2026-01-15 13:19:34 +00:00
if ( ! empty ( $countryCode )) {
$queueForStatsUsage
-> addMetric ( str_replace ( '{countryCode}' , $countryCode , METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE ), 1 );
}
} catch ( NumberParseException $e ) {
// Ignore invalid phone number for country code stats
2025-01-08 11:31:01 +00:00
}
2026-01-15 12:59:30 +00:00
$queueForStatsUsage
-> addMetric ( METRIC_AUTH_METHOD_PHONE , 1 )
-> setProject ( $project )
-> trigger ();
2020-06-29 21:43:34 +00:00
}
2019-07-21 11:43:06 +00:00
2024-05-22 02:11:06 +00:00
$verification -> setAttribute ( 'secret' , $secret );
2019-05-09 06:54:39 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-06-08 12:50:31 +00:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $verification -> getId ())
2024-04-03 03:36:53 +00:00
-> setPayload ( $response -> output ( $verification , Response :: MODEL_TOKEN ), sensitive : [ 'secret' ]);
2019-05-09 06:54:39 +00:00
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
-> dynamic ( $verification , Response :: MODEL_TOKEN );
2020-12-26 14:31:53 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/verifications/phone' )
2025-10-07 04:28:39 +00:00
-> alias ( '/v1/account/verification/phone' )
2024-06-21 14:41:46 +00:00
-> desc ( 'Update phone verification (confirmation)' )
2022-06-08 12:50:31 +00:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'public' )
-> label ( 'event' , 'users.[userId].verification.[tokenId].update' )
2022-09-05 08:00:08 +00:00
-> label ( 'audits.event' , 'verification.update' )
2022-08-11 13:19:05 +00:00
-> label ( 'audits.resource' , 'user/{response.userId}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'verification' ,
2025-01-17 04:31:39 +00:00
name : 'updatePhoneVerification' ,
description : '/docs/references/account/update-phone-verification.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TOKEN ,
)
],
contentType : ContentType :: JSON
))
2022-06-08 12:50:31 +00:00
-> label ( 'abuse-limit' , 10 )
-> label ( 'abuse-key' , 'userId:{param-userId}' )
-> param ( 'userId' , '' , new UID (), 'User ID.' )
-> param ( 'secret' , '' , new Text ( 256 ), 'Valid verification token.' )
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2025-11-04 03:48:57 +00:00
-> inject ( 'proofForCode' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $userId , string $secret , Response $response , User $user , Database $dbForProject , Event $queueForEvents , ProofsCode $proofForCode , Authorization $authorization ) {
2025-11-04 03:48:57 +00:00
/** @var Appwrite\Utopia\Database\Documents\User $profile */
2026-01-14 15:08:00 +00:00
$profile = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'users' , $userId ));
2022-06-08 12:50:31 +00:00
if ( $profile -> isEmpty ()) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_NOT_FOUND );
2023-05-31 20:52:05 +00:00
}
2022-06-08 12:50:31 +00:00
2025-11-04 03:48:57 +00:00
$verifiedToken = $profile -> tokenVerify ( TOKEN_TYPE_PHONE , $secret , $proofForCode );
2022-06-08 12:50:31 +00:00
2023-10-05 10:18:19 +00:00
if ( ! $verifiedToken ) {
2022-07-26 14:24:32 +00:00
throw new Exception ( Exception :: USER_INVALID_TOKEN );
2022-06-08 12:50:31 +00:00
}
2026-01-14 15:08:00 +00:00
$authorization -> addRole ( Role :: user ( $profile -> getId ()) -> toString ());
2022-06-08 12:50:31 +00:00
$profile = $dbForProject -> updateDocument ( 'users' , $profile -> getId (), $profile -> setAttribute ( 'phoneVerification' , true ));
2023-07-07 00:12:39 +00:00
$user -> setAttributes ( $profile -> getArrayCopy ());
2019-05-09 06:54:39 +00:00
2023-10-05 10:18:19 +00:00
$verificationDocument = $dbForProject -> getDocument ( 'tokens' , $verifiedToken -> getId ());
2019-05-09 06:54:39 +00:00
2022-06-08 12:50:31 +00:00
/**
* We act like we 're updating and validating the verification token but actually we don' t need it anymore .
*/
2023-10-05 10:18:19 +00:00
$dbForProject -> deleteDocument ( 'tokens' , $verifiedToken -> getId ());
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $profile -> getId ());
2019-05-09 06:54:39 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-06-08 12:50:31 +00:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'tokenId' , $verificationDocument -> getId ())
;
2020-01-11 13:58:02 +00:00
2022-06-08 12:50:31 +00:00
$response -> dynamic ( $verificationDocument , Response :: MODEL_TOKEN );
2020-12-26 14:31:53 +00:00
});
2019-05-09 06:54:39 +00:00
2026-02-04 05:30:22 +00:00
Http :: post ( '/v1/account/targets/push' )
2024-02-26 02:25:45 +00:00
-> desc ( 'Create push target' )
2024-02-02 14:05:51 +00:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'targets.write' )
-> label ( 'audits.event' , 'target.create' )
-> label ( 'audits.resource' , 'target/response.$id' )
-> label ( 'event' , 'users.[userId].targets.[targetId].create' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'pushTargets' ,
2025-01-17 04:31:39 +00:00
name : 'createPushTarget' ,
2025-01-17 07:44:25 +00:00
description : '/docs/references/account/create-push-target.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_CREATED ,
model : Response :: MODEL_TARGET ,
)
],
contentType : ContentType :: JSON
))
2024-02-02 14:05:51 +00:00
-> param ( 'targetId' , '' , new CustomId (), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.' )
-> param ( 'identifier' , '' , new Text ( Database :: LENGTH_KEY ), 'The target identifier (token, email, phone etc.)' )
-> param ( 'providerId' , '' , new UID (), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.' , true )
-> inject ( 'queueForEvents' )
-> inject ( 'user' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'dbForProject' )
2025-11-04 03:48:57 +00:00
-> inject ( 'store' )
-> inject ( 'proofForToken' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $targetId , string $identifier , string $providerId , Event $queueForEvents , User $user , Request $request , Response $response , Database $dbForProject , Store $store , ProofsToken $proofForToken , Authorization $authorization ) {
2024-02-02 14:05:51 +00:00
$targetId = $targetId == 'unique()' ? ID :: unique () : $targetId ;
2023-08-28 12:19:37 +00:00
2026-01-14 15:08:00 +00:00
$provider = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'providers' , $providerId ));
2023-08-30 04:30:44 +00:00
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'targets' , $targetId ));
2023-08-29 09:40:30 +00:00
2024-02-02 14:05:51 +00:00
if ( ! $target -> isEmpty ()) {
throw new Exception ( Exception :: USER_TARGET_ALREADY_EXISTS );
2023-04-19 08:29:29 +00:00
}
2024-02-02 14:05:51 +00:00
$detector = new Detector ( $request -> getUserAgent ());
$detector -> skipBotDetection (); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
2023-05-29 13:58:45 +00:00
2024-02-02 14:05:51 +00:00
$device = $detector -> getDevice ();
2020-06-29 21:43:34 +00:00
2025-11-04 03:48:57 +00:00
$sessionId = $user -> sessionVerify ( $store -> getProperty ( 'secret' , '' ), $proofForToken );
2024-02-02 14:05:51 +00:00
$session = $dbForProject -> getDocument ( 'sessions' , $sessionId );
try {
$target = $dbForProject -> createDocument ( 'targets' , new Document ([
'$id' => $targetId ,
'$permissions' => [
Permission :: read ( Role :: user ( $user -> getId ())),
Permission :: update ( Role :: user ( $user -> getId ())),
Permission :: delete ( Role :: user ( $user -> getId ())),
],
'providerId' => ! empty ( $providerId ) ? $providerId : null ,
2025-05-26 05:42:11 +00:00
'providerInternalId' => ! empty ( $providerId ) ? $provider -> getSequence () : null ,
2025-09-18 07:03:42 +00:00
'providerType' => MESSAGE_TYPE_PUSH ,
2024-02-02 14:05:51 +00:00
'userId' => $user -> getId (),
2025-05-26 05:42:11 +00:00
'userInternalId' => $user -> getSequence (),
2024-02-02 14:05:51 +00:00
'sessionId' => $session -> getId (),
2025-05-26 05:42:11 +00:00
'sessionInternalId' => $session -> getSequence (),
2024-02-02 14:05:51 +00:00
'identifier' => $identifier ,
'name' => " { $device [ 'deviceBrand' ] } { $device [ 'deviceModel' ] } "
]));
} catch ( Duplicate ) {
throw new Exception ( Exception :: USER_TARGET_ALREADY_EXISTS );
}
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2020-11-18 19:38:31 +00:00
2024-02-02 14:05:51 +00:00
$queueForEvents
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'targetId' , $target -> getId ());
2020-11-18 19:38:31 +00:00
2022-09-07 11:11:10 +00:00
$response
-> setStatusCode ( Response :: STATUS_CODE_CREATED )
2024-02-02 14:05:51 +00:00
-> dynamic ( $target , Response :: MODEL_TARGET );
2020-12-26 14:31:53 +00:00
});
2020-01-12 00:20:35 +00:00
2026-02-04 05:30:22 +00:00
Http :: put ( '/v1/account/targets/:targetId/push' )
2024-02-26 02:25:45 +00:00
-> desc ( 'Update push target' )
2020-06-25 18:32:12 +00:00
-> groups ([ 'api' , 'account' ])
2024-02-02 14:05:51 +00:00
-> label ( 'scope' , 'targets.write' )
2023-11-15 20:42:06 +00:00
-> label ( 'audits.event' , 'target.update' )
-> label ( 'audits.resource' , 'target/response.$id' )
2023-11-16 11:17:36 +00:00
-> label ( 'event' , 'users.[userId].targets.[targetId].update' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'pushTargets' ,
2025-01-17 04:31:39 +00:00
name : 'updatePushTarget' ,
2025-01-17 07:44:25 +00:00
description : '/docs/references/account/update-push-target.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_TARGET ,
)
],
contentType : ContentType :: JSON
))
2023-11-15 20:42:06 +00:00
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
2023-11-16 11:39:08 +00:00
-> param ( 'identifier' , '' , new Text ( Database :: LENGTH_KEY ), 'The target identifier (token, email, phone etc.)' )
2023-11-15 20:42:06 +00:00
-> inject ( 'queueForEvents' )
2020-12-26 14:31:53 +00:00
-> inject ( 'user' )
2023-11-15 20:42:06 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
2021-12-27 12:45:23 +00:00
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $targetId , string $identifier , Event $queueForEvents , Document $user , Request $request , Response $response , Database $dbForProject , Authorization $authorization ) {
2020-06-29 21:43:34 +00:00
2026-01-14 15:08:00 +00:00
$target = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'targets' , $targetId ));
2020-06-29 21:43:34 +00:00
2023-11-15 20:42:06 +00:00
if ( $target -> isEmpty ()) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
2020-06-29 21:43:34 +00:00
}
2020-01-12 00:20:35 +00:00
2023-11-15 20:42:06 +00:00
if ( $user -> getId () !== $target -> getAttribute ( 'userId' )) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
}
2020-01-12 00:20:35 +00:00
2023-11-15 20:42:06 +00:00
if ( $identifier ) {
2024-10-22 01:08:16 +00:00
$target
-> setAttribute ( 'identifier' , $identifier )
2024-10-22 01:54:34 +00:00
-> setAttribute ( 'expired' , false );
2020-06-29 21:43:34 +00:00
}
2020-01-12 00:20:35 +00:00
2023-11-16 10:56:36 +00:00
$detector = new Detector ( $request -> getUserAgent ());
$detector -> skipBotDetection (); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
2020-01-12 00:20:35 +00:00
2023-11-16 10:56:36 +00:00
$device = $detector -> getDevice ();
2020-01-12 00:20:35 +00:00
2023-11-16 11:17:36 +00:00
$target -> setAttribute ( 'name' , " { $device [ 'deviceBrand' ] } { $device [ 'deviceModel' ] } " );
2023-07-07 00:12:39 +00:00
2023-11-15 20:42:06 +00:00
$target = $dbForProject -> updateDocument ( 'targets' , $target -> getId (), $target );
2020-01-12 00:20:35 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2021-05-06 22:31:05 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2023-11-15 20:42:06 +00:00
-> setParam ( 'userId' , $user -> getId ())
-> setParam ( 'targetId' , $target -> getId ());
2022-04-04 06:30:07 +00:00
2023-11-15 20:42:06 +00:00
$response
-> dynamic ( $target , Response :: MODEL_TARGET );
2021-06-12 14:50:40 +00:00
});
2022-06-08 12:50:31 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/account/targets/:targetId/push' )
2024-02-26 02:25:45 +00:00
-> desc ( 'Delete push target' )
2024-01-22 17:29:10 +00:00
-> groups ([ 'api' , 'account' ])
2024-02-02 14:05:51 +00:00
-> label ( 'scope' , 'targets.write' )
-> label ( 'audits.event' , 'target.delete' )
-> label ( 'audits.resource' , 'target/response.$id' )
-> label ( 'event' , 'users.[userId].targets.[targetId].delete' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'pushTargets' ,
2025-01-17 04:31:39 +00:00
name : 'deletePushTarget' ,
2025-01-17 07:44:25 +00:00
description : '/docs/references/account/delete-push-target.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2024-02-02 14:05:51 +00:00
-> param ( 'targetId' , '' , new UID (), 'Target ID.' )
-> inject ( 'queueForEvents' )
-> inject ( 'queueForDeletes' )
2024-01-22 17:29:10 +00:00
-> inject ( 'user' )
2022-06-08 12:50:31 +00:00
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'dbForProject' )
2026-01-14 15:08:00 +00:00
-> inject ( 'authorization' )
-> action ( function ( string $targetId , Event $queueForEvents , Delete $queueForDeletes , Document $user , Request $request , Response $response , Database $dbForProject , Authorization $authorization ) {
$target = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'targets' , $targetId ));
2022-06-08 12:50:31 +00:00
2024-02-02 14:05:51 +00:00
if ( $target -> isEmpty ()) {
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
2023-09-22 17:23:41 +00:00
}
2022-06-08 12:50:31 +00:00
2025-05-26 05:42:11 +00:00
if ( $user -> getSequence () !== $target -> getAttribute ( 'userInternalId' )) {
2024-02-02 14:05:51 +00:00
throw new Exception ( Exception :: USER_TARGET_NOT_FOUND );
2024-02-11 14:51:19 +00:00
}
2024-02-11 14:58:05 +00:00
2024-02-02 14:05:51 +00:00
$dbForProject -> deleteDocument ( 'targets' , $target -> getId ());
2023-04-19 08:29:29 +00:00
2024-02-02 14:05:51 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $user -> getId ());
2022-06-08 12:50:31 +00:00
2024-01-22 17:29:10 +00:00
$queueForDeletes
2024-02-02 14:05:51 +00:00
-> setType ( DELETE_TYPE_TARGET )
-> setDocument ( $target );
2022-06-08 12:50:31 +00:00
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-06-08 12:50:31 +00:00
-> setParam ( 'userId' , $user -> getId ())
2024-02-02 14:05:51 +00:00
-> setParam ( 'targetId' , $target -> getId ())
-> setPayload ( $response -> output ( $target , Response :: MODEL_TARGET ));
2022-06-08 12:50:31 +00:00
2024-01-22 17:29:10 +00:00
$response -> noContent ();
2022-06-08 12:50:31 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: get ( '/v1/account/identities' )
2024-09-03 16:22:30 +00:00
-> desc ( 'List identities' )
2022-06-08 12:50:31 +00:00
-> groups ([ 'api' , 'account' ])
2024-03-06 18:07:58 +00:00
-> label ( 'scope' , 'account' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'identities' ,
2025-01-17 04:31:39 +00:00
name : 'listIdentities' ,
description : '/docs/references/account/list-identities.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_OK ,
model : Response :: MODEL_IDENTITY_LIST ,
)
],
contentType : ContentType :: JSON
))
2024-03-06 18:07:58 +00:00
-> param ( 'queries' , [], new Identities (), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode ( ', ' , Identities :: ALLOWED_ATTRIBUTES ), true )
2025-11-05 03:33:02 +00:00
-> param ( 'total' , true , new Boolean ( true ), 'When set to false, the total count returned will be 0 and will not be calculated.' , true )
2022-06-08 12:50:31 +00:00
-> inject ( 'response' )
-> inject ( 'user' )
-> inject ( 'dbForProject' )
2025-11-05 03:33:02 +00:00
-> action ( function ( array $queries , bool $includeTotal , Response $response , User $user , Database $dbForProject ) {
2022-06-08 12:50:31 +00:00
2024-03-06 18:07:58 +00:00
try {
$queries = Query :: parseQueries ( $queries );
} catch ( QueryException $e ) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $e -> getMessage ());
2022-06-08 12:50:31 +00:00
}
2025-05-26 05:42:11 +00:00
$queries [] = Query :: equal ( 'userInternalId' , [ $user -> getSequence ()]);
2022-06-08 12:50:31 +00:00
2026-01-28 12:53:24 +00:00
$cursor = Query :: getCursorQueries ( $queries , false );
$cursor = \reset ( $cursor );
2024-10-17 05:41:24 +00:00
2026-01-28 12:53:24 +00:00
if ( $cursor !== false ) {
2024-10-17 05:41:24 +00:00
$validator = new Cursor ();
if ( ! $validator -> isValid ( $cursor )) {
throw new Exception ( Exception :: GENERAL_QUERY_INVALID , $validator -> getDescription ());
}
2024-03-06 18:07:58 +00:00
$identityId = $cursor -> getValue ();
$cursorDocument = $dbForProject -> getDocument ( 'identities' , $identityId );
2022-06-08 12:50:31 +00:00
2024-03-06 18:07:58 +00:00
if ( $cursorDocument -> isEmpty ()) {
throw new Exception ( Exception :: GENERAL_CURSOR_NOT_FOUND , " Identity ' { $identityId } ' for the 'cursor' value not found. " );
}
2023-07-07 00:12:39 +00:00
2024-03-06 18:07:58 +00:00
$cursor -> setValue ( $cursorDocument );
}
2022-06-08 12:50:31 +00:00
2024-03-06 18:07:58 +00:00
$filterQueries = Query :: groupByType ( $queries )[ 'filters' ];
2025-04-16 11:59:36 +00:00
try {
$results = $dbForProject -> find ( 'identities' , $queries );
} catch ( OrderException $e ) {
2025-04-17 04:46:26 +00:00
throw new Exception ( Exception :: DATABASE_QUERY_ORDER_NULL , " The order attribute ' { $e -> getAttribute () } ' had a null value. Cursor pagination requires all documents order attribute values are non-null. " );
2025-04-16 11:59:36 +00:00
}
2025-11-05 03:33:02 +00:00
$total = $includeTotal ? $dbForProject -> count ( 'identities' , $filterQueries , APP_LIMIT_COUNT ) : 0 ;
2022-06-08 12:50:31 +00:00
2024-03-06 18:07:58 +00:00
$response -> dynamic ( new Document ([
'identities' => $results ,
2025-10-29 07:17:10 +00:00
'total' => $total ,
2024-03-06 18:07:58 +00:00
]), Response :: MODEL_IDENTITY_LIST );
2022-06-08 12:50:31 +00:00
});
2024-01-09 11:58:36 +00:00
2026-02-04 05:30:22 +00:00
Http :: delete ( '/v1/account/identities/:identityId' )
2024-03-06 18:07:58 +00:00
-> desc ( 'Delete identity' )
2024-01-09 11:58:36 +00:00
-> groups ([ 'api' , 'account' ])
-> label ( 'scope' , 'account' )
2024-03-06 18:07:58 +00:00
-> label ( 'event' , 'users.[userId].identities.[identityId].delete' )
-> label ( 'audits.event' , 'identity.delete' )
-> label ( 'audits.resource' , 'identity/{request.$identityId}' )
-> label ( 'audits.userId' , '{user.$id}' )
2025-01-17 04:31:39 +00:00
-> label ( 'sdk' , new Method (
namespace : 'account' ,
2025-03-31 05:48:17 +00:00
group : 'identities' ,
2025-01-17 04:31:39 +00:00
name : 'deleteIdentity' ,
description : '/docs/references/account/delete-identity.md' ,
2025-12-13 16:06:44 +00:00
auth : [ AuthType :: ADMIN , AuthType :: SESSION , AuthType :: JWT ],
2025-01-17 04:31:39 +00:00
responses : [
new SDKResponse (
code : Response :: STATUS_CODE_NOCONTENT ,
model : Response :: MODEL_NONE ,
)
],
contentType : ContentType :: NONE
))
2024-03-06 18:07:58 +00:00
-> param ( 'identityId' , '' , new UID (), 'Identity ID.' )
2024-01-09 11:58:36 +00:00
-> inject ( 'response' )
-> inject ( 'dbForProject' )
-> inject ( 'queueForEvents' )
2024-03-06 18:07:58 +00:00
-> action ( function ( string $identityId , Response $response , Database $dbForProject , Event $queueForEvents ) {
2024-01-09 11:58:36 +00:00
2024-03-06 18:07:58 +00:00
$identity = $dbForProject -> getDocument ( 'identities' , $identityId );
2024-01-28 02:10:14 +00:00
2024-03-06 18:07:58 +00:00
if ( $identity -> isEmpty ()) {
throw new Exception ( Exception :: USER_IDENTITY_NOT_FOUND );
}
2024-01-09 11:58:36 +00:00
2024-03-06 18:07:58 +00:00
$dbForProject -> deleteDocument ( 'identities' , $identityId );
2024-01-09 11:58:36 +00:00
$queueForEvents
2024-03-06 18:07:58 +00:00
-> setParam ( 'userId' , $identity -> getAttribute ( 'userId' ))
-> setParam ( 'identityId' , $identity -> getId ())
-> setPayload ( $response -> output ( $identity , Response :: MODEL_IDENTITY ));
2024-01-09 11:58:36 +00:00
2024-03-06 18:07:58 +00:00
return $response -> noContent ();
2025-01-17 04:39:16 +00:00
});