###############
User management
###############

The Firebase Admin SDK for PHP provides an API for managing your Firebase users with elevated privileges.
The admin user management API gives you the ability to programmatically retrieve, create, update, and
delete users without requiring a user's existing credentials and without worrying about client-side
rate limiting.

************
User Records
************

``UserRecord`` s returned by methods from ``Kreait\Firebase\Contract\Auth`` class have the
following signature:

.. code-block:: json

    {
        "uid": "jEazVdPDhqec0tnEOG7vM5wbDyU2",
        "email": "user@example.com",
        "emailVerified": true,
        "displayName": null,
        "photoUrl": null,
        "phoneNumber": null,
        "disabled": false,
        "metadata": {
            "createdAt": "2018-02-14T15:41:32+00:00",
            "lastLoginAt": "2018-02-14T15:41:32+00:00",
            "passwordUpdatedAt": "2018-02-14T15:42:19+00:00",
            "lastRefreshAt": "2018-02-14T15:42:19+00:00"
        },
        "providerData": [
            {
                "uid": "user@example.com",
                "displayName": null,
                "screenName": null,
                "email": "user@example.com",
                "photoUrl": null,
                "providerId": "password",
                "phoneNumber": null
            }
        ],
        "passwordHash": "UkVEQUNURUQ=",
        "customClaims": null,
        "tokensValidAfterTime": "2018-02-14T15:41:32+00:00"
    }

**********
List users
**********

To enhance performance and prevent memory issues when retrieving a huge amount of users,
this methods returns a `Generator <https://www.php.net/manual/en/language.generators.overview.php>`_.

.. code-block:: php

    $users = $auth->listUsers($defaultMaxResults = 1000, $defaultBatchSize = 1000);

    foreach ($users as $user) {
        /** @var \Kreait\Firebase\Auth\UserRecord $user */
        // ...
    }
    // or
    array_map(function (\Kreait\Firebase\Auth\UserRecord $user) {
        // ...
    }, iterator_to_array($users));


***********
Query users
***********

Listing all users with ``listUser()`` is fast an memory-efficient if you want to process a large
number of users. However, if you prefer paginating over subsets of users with more parameters,
you can use the ``queryUsers()`` method.

User queries can be created in two ways: by building a ``UserQuery`` object or by passing an array.

The following two snippets show all possible query modifiers with both ways:

.. code-block:: php

    use Kreait\Firebase\Auth\UserQuery;

    # Building a user query object
    $userQuery = UserQuery::all()
        ->sortedBy(UserQuery::FIELD_USER_EMAIL)
        ->inDescendingOrder()
        // ->inAscendingOrder() # this is the default
        ->withOffset(1)
        ->withLimit(499); # The maximum supported limit is 500

    # Using an array
    $userQuery = [
        'sortBy' => UserQuery::FIELD_USER_EMAIL,
        'order' => UserQuery::ORDER_DESC,
        // 'order' => UserQuery::ORDER_DESC # this is the default
        'offset' => 1,
        'limit' => 499, # The maximum supported limit is 500
    ];

It is possible to sort by the following fields:

* ``UserQuery::FIELD_CREATED_AT``
* ``UserQuery::FIELD_LAST_LOGIN_AT``
* ``UserQuery::FIELD_NAME``
* ``UserQuery::FIELD_USER_EMAIL``
* ``UserQuery::FIELD_USER_ID``

.. code-block:: php

    $users = $auth->queryUsers($userQuery);

You can also filter by email, phone number or uid:

.. code-block:: php

    use Kreait\Firebase\Auth\UserQuery;

    $userQuery = UserQuery::all()->withFilter(UserQuery::FILTER_EMAIL, '<email>');
    $userQuery = UserQuery::all()->withFilter(UserQuery::FILTER_PHONE_NUMBER, '<phone number>');
    $userQuery = UserQuery::all()->withFilter(UserQuery::FILTER_UID, '<uid>');

    $userQuery = ['filter' => [UserQuery::FILTER_EMAIL => '<email>'];
    $userQuery = ['filter' => [UserQuery::FILTER_PHONE_NUMBER => '<email>'];
    $userQuery = ['filter' => [UserQuery::FILTER_UID => '<email>'];

A user query will always return an array of ``UserRecord`` s. If none could be found, the array
will be empty.

.. note::
    Filters don't support partial matches, and only one filter can be applied at the same time.
    If you specify multiple filters, only the last one will be submitted.


*************************************
Get information about a specific user
*************************************

.. code-block:: php

    try {
        $user = $auth->getUser('some-uid');
        $user = $auth->getUserByEmail('user@example.com');
        $user = $auth->getUserByPhoneNumber('+49-123-456789');
        // For `getUserByProviderUid()` please see the section below.
        $user = $auth->getUserByProviderUid('google.com', 'google-uid');
    } catch (\Kreait\Firebase\Exception\Auth\UserNotFound $e) {
        echo $e->getMessage();
    }

***************************************************
Get information about a user by federated provider
***************************************************

You can retrieve a user by their federated identity provider UID (e.g. Google, Facebook, etc.):

.. code-block:: php

    try {
        $googleUser = $auth->getUserByProviderUid('google.com', 'google-uid');
        $facebookUser = $auth->getUserByProviderUid('facebook.com', 'facebook-uid');
    } catch (\Kreait\Firebase\Exception\Auth\UserNotFound $e) {
        echo $e->getMessage();
    }

.. note::
    Since this method couldn't be added to the ``Kreait\Firebase\Contract\Auth`` interface without causing a breaking
    change, a new transitional interface/contract named ``Kreait\Firebase\Contract\Transitional\FederatedUserFetcher``
    was added. This interface will be removed in the next major version of the SDK.

There are several ways to check if you can use the ``getUserByProviderUid()`` method:

.. code-block:: php

    use Kreait\Firebase\Contract\Transitional\FederatedUserFetcher;
    use Kreait\Firebase\Factory;

    $auth = (new Factory())->createAuth();

    if (method_exists($auth, 'getUserByProviderUid')) {
        $user = $auth->getUserByProviderUid('google.com', 'google-uid');
    }

    if ($auth instanceof \Kreait\Firebase\Auth) { // This is the implementation, not the interface
        $user = $auth->getUserByProviderUid('google.com', 'google-uid');
    }

    if ($auth instanceof FederatedUserFetcher) {
        $user = $auth->getUserByProviderUid('google.com', 'google-uid');
    }

************************************
Get information about multiple users
************************************

You can retrieve multiple user records by using ``$auth->getUsers()``. When a user doesn't exist,
no exception is thrown, but its entry in the result set is null:

.. code-block:: php

    $users = $auth->getUsers(['some-uid', 'another-uid', 'non-existing-uid']);

Result:

.. code-block:: text

    [
        'some-uid' => <UserRecord>,
        'another-uid' => <UserRecord>,
        'non-existing-uid' => null
    ]

*************
Create a user
*************

The Admin SDK provides a method that allows you to create a new Firebase Authentication user.
This method accepts an object containing the profile information to include in the newly created user account:

.. code-block:: php

    $userProperties = [
        'email' => 'user@example.com',
        'emailVerified' => false,
        'phoneNumber' => '+15555550100',
        'password' => 'secretPassword',
        'displayName' => 'John Doe',
        'photoUrl' => 'http://www.example.com/12345678/photo.png',
        'disabled' => false,
    ];

    $createdUser = $auth->createUser($userProperties);

    // This is equivalent to:

    $request = \Kreait\Auth\Request\CreateUser::new()
        ->withUnverifiedEmail('user@example.com')
        ->withPhoneNumber('+15555550100')
        ->withClearTextPassword('secretPassword')
        ->withDisplayName('John Doe')
        ->withPhotoUrl('http://www.example.com/12345678/photo.png');

    $createdUser = $auth->createUser($request);

By default, Firebase Authentication will generate a random uid for the new user.
If you instead want to specify your own uid for the new user, you can include
in the properties passed to the user creation method:

.. code-block:: php

    $properties = [
        'uid' => 'some-uid',
        // other properties
    ];

    $request = \Kreait\Auth\Request\CreateUser::new()
        ->withUid('some-uid')
        // with other properties
    ;

Any combination of the following properties can be provided:

================= ======= ===========
Property          Type    Description
================= ======= ===========
``uid``           string  The uid to assign to the newly created user. Must be a string between 1 and 128 characters long, inclusive. If not provided, a random uid will be automatically generated.
``email``         string  The user's primary email. Must be a valid email address.
``emailVerified`` boolean Whether or not the user's primary email is verified. If not provided, the default is false.
``phoneNumber``   string  The user's primary phone number. Must be a valid E.164 spec compliant phone number.
``password``      string  The user's raw, unhashed password. Must be at least six characters long.
``displayName``   string  The users' display name.
``photoURL``      string  The user's photo URL.
``disabled``      boolean Whether or not the user is disabled. true for disabled; false for enabled. If not provided, the default is false.
================= ======= ===========

.. note::
    All of the above properties are optional. If a certain property is not specified,
    the value for that property will be empty unless a default is mentioned
    in the above table.

.. note::
    If you provide none of the properties, an anonymous user will be created.

*************
Update a user
*************

Updating a user works exactly as creating a new user, except that the ``uid`` property is required:

.. code-block:: php

    $uid = 'some-uid';
    $properties = [
        'displayName' => 'New display name'
    ];

    $updatedUser = $auth->updateUser($uid, $properties);

    $request = \Kreait\Auth\Request\UpdateUser::new()
        ->withDisplayName('New display name');

    $updatedUser = $auth->updateUser($uid, $request);

In addition to the properties of a create request, the following properties can be provided:

====================== ============ ===========
Property               Type         Description
====================== ============ ===========
``deleteEmail``        boolean      Whether or not to delete the user's email.
``deletePhotoUrl``     boolean      Whether or not to delete the user's photo.
``deleteDisplayName``  boolean      Whether or not to delete the user's display name.
``deletePhoneNumber``  boolean      Whether or not to delete the user's phone number.
``resetMultiFactor``   boolean      Whether or not to reset all of the user's enrolled factors. Including phone and TOTP factors.
``multiFactors``       array        An array of multi-factor factors.
``deleteProvider``     string|array One or more identity providers to delete.
``customAttributes``   array        A list of custom attributes which will be available in a User's ID token.
====================== ============ ===========

.. note::

    When deleting the email from an existing user, the password authentication provider
    will be disabled (the user can't log in with an email and password combination
    anymore). After adding a new email to the same user, the previously set password
    might be restored. If you just want to change a user's email, consider updating
    the email field directly.

************************
Change a user's password
************************

.. code-block:: php

    $uid = 'some-uid';

    $updatedUser = $auth->changeUserPassword($uid, 'new password');

*********************
Change a user's email
*********************

.. code-block:: php

    $uid = 'some-uid';

    $updatedUser = $auth->changeUserEmail($uid, 'user@example.com');

**************
Disable a user
**************

.. code-block:: php

    $uid = 'some-uid';

    $updatedUser = $auth->disableUser($uid);


*************
Enable a user
*************

.. code-block:: php

    $uid = 'some-uid';

    $updatedUser = $auth->enableUser($uid);

******************
Custom user claims
******************

.. note::

    Learn more about custom attributes/claims in the official documentation:
    `Control Access with Custom Claims and Security Rules <https://firebase.google.com/docs/auth/admin/custom-claims>`_

.. code-block:: php

    // The new custom claims will propagate to the user's ID token the
    // next time a new one is issued.
    $auth->setCustomUserClaims($uid, ['admin' => true, 'key1' => 'value1']);

    // Retrieve a user's current custom claims
    $claims = $auth->getUser($uid)->customClaims;

    // Remove a user's custom claims
    $auth->setCustomUserClaims($uid, null);

The custom claims object should not contain any `OIDC <https://openid.net/specs/openid-connect-core-1_0.html#IDToken>`_
reserved key names or Firebase reserved names. Custom claims payload must not exceed 1000 bytes.

*************
Delete a user
*************

The Firebase Admin SDK allows deleting users by their ``uid``:

.. code-block:: php

    $uid = 'some-uid';

    try {
        $auth->deleteUser($uid);
    catch (\Kreait\Firebase\Exception\Auth\UserNotFound $e) {
        echo $e->getMessage();
    } catch (\Kreait\Firebase\Exception\AuthException $e) {
        echo 'Deleting
    }

This method returns nothing when the deletion completes successfully. If the provided ``uid`` does not correspond
to an existing user or the user cannot be deleted for any other reason, the delete user method throws an error.

*********************
Delete multiple users
*********************

The Firebase Admin SDK can also delete multiple (up to 1000) users at once:

.. code-block:: php

    $uid = ['uid-1', 'uid-2', 'uid-3'];
    $forceDeleteEnabledUsers = true; // default: false

    $result = $auth->deleteUsers($uids, $forceDeleteEnabledUsers);

By default, only disabled users will be deleted. If you want to also delete enabled users,
use ``true`` as the second argument.

This method always returns an instance of ``Kreait\Firebase\Auth\DeleteUsersResult``:

.. code-block:: php

    $successCount = $result->successCount();
    $failureCount = $result->failureCount();
    $errors = $result->rawErrors();

.. note::
    Using this method to delete multiple users at once will not trigger ``onDelete()`` event handlers for
    Cloud Functions for Firebase. This is because batch deletes do not trigger a user deletion event on each user.
    Delete users one at a time if you want user deletion events to fire for each deleted user.

*********************************
Set multi factor authentication
*********************************

The Firebase Admin SDK allows setting multi-factor authentication for a user, consisting of phone factors. Setting the
multi-factor authentication overwrites all existing factors. Setting the `mfaEnrollmentId` and `enrolledAt` properties is
optional. For example:

.. code-block:: php

    $uid = 'some-uid';

    $updatedUser = $auth->updateUser($uid, ['multifactors' => [[
        'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed',
        'phoneInfo' => '+31123456789',
        'displayName' => 'foo',
        'enrolledAt' => '2025-02-28T15:30:00Z',
    ]]);

**************************************
Duplicate/Unregistered email addresses
**************************************

Some Firebase Authentication methods that take email addresses as parameters throw specific errors if the email address
is unregistered when it must be registered (for example, when signing in with an email address and password), or
registered when it must be unused (for example, when changing a user's email address).

If you try to create a new user, but the given email address has already been used before, the Firebase API returns
an ``EMAIL_EXISTS`` error. On the other hand, if you try to sign in a user with an email address that hasn't been
registered, the API returns an ``EMAIL_NOT_FOUND`` error.

You can handle both cases with the SDK:

.. code-block:: php

    try {
        $user = $auth->createUser([
            'email' => $email,
        ]);
    } catch (\Kreait\Firebase\Exception\Auth\EmailExists $e) {
        echo $e->getMessage(); // "The email address is already in use by another account"
    }

    try {
        $signInResult = $auth->signInWithEmailAndPassword($email, $password);
    } catch (\Kreait\Firebase\Exception\Auth\EmailNotFound $e) {
        echo $e->getMessage(); // "There is no user record corresponding to this identifier. The user may have been deleted."
    }

.. note::
    Checking for existing/non-existing email addresses can be helpful for suggesting specific remedies to users, but
    it can also be abused by malicious actors to discover the email addresses registered by your users.

    To mitigate this risk, Firebase recommends you
    `enable email enumeration protection <https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection>`_
    for your project using the Google Cloud ``gcloud`` tool. Note that enabling this feature changes
    Firebase Authentication's error reporting behavior: be sure your app doesn't rely on the more specific errors.


************************
Using Email Action Codes
************************

The Firebase Admin SDK provides the ability to send users emails containing links they can use for password resets,
email address verification, and email-based sign-in. These emails are sent by Google and have limited
customizability.

If you want to instead use your own email templates and your own email delivery service, you can use the
Firebase Admin SDK to programmatically generate the action links for the above flows, which you can
include in emails to your users.

Action Code Settings
====================

.. note::
    Action Code Settings are optional.

Action Code Settings allow you to pass additional state via a continue URL which is accessible after the user clicks
the email link. This also provides the user the ability to go back to the app after the action is completed.
In addition, you can specify whether to handle the email action link directly from a mobile application
when it is installed or from a browser.

For links that are meant to be opened via a mobile app, you’ll need to enable Firebase Dynamic Links and perform some
tasks to detect these links from your mobile app. Refer to the instructions on how to
`configure Firebase Dynamic Links <https://firebase.google.com/docs/auth/web/passing-state-in-email-actions#configuring-hosting-links>`_
for email actions.

========================= =========== ===========
Parameter                 Type        Description
========================= =========== ===========
``continueUrl``           string|null Sets the continue URL
``url``                   string|null Alias for ``continueUrl``
``handleCodeInApp``       bool|null    | Whether the email action link will be opened in a mobile app or a web link first.
                                       | The default is false. When set to true, the action code link will be be sent
                                       | as a Universal Link or Android App Link and will be opened by the app if
                                       | installed. In the false case, the code will be sent to the web widget first
                                       | and then on continue will redirect to the app if installed.
``androidPackageName``    string|null  | Sets the Android package name. This will try to open the link in an android app
                                       | if it is installed.
``androidInstallApp``     bool|null    | Whether to install the Android app if the device supports it and the app is not
                                       | already installed. If this field is provided without a ``androidPackageName``,
                                       | an error is thrown explaining that the packageName must be provided in
                                       | conjunction with this field.
``androidMinimumVersion`` string|null  | If specified, and an older version of the app is installed,
                                       | the user is taken to the Play Store to upgrade the app.
                                       | The Android app needs to be registered in the Console.
``iOSBundleId``           string|null  | Sets the iOS bundle ID. This will try to open the link in an iOS app if it is
                                       | installed. The iOS app needs to be registered in the Console.
========================= =========== ===========

Example:

.. code-block:: php

    $actionCodeSettings = [
        'continueUrl' => 'https://www.example.com/checkout?cartId=1234',
        'handleCodeInApp' => true,
        'dynamicLinkDomain' => 'coolapp.page.link',
        'androidPackageName' => 'com.example.android',
        'androidMinimumVersion' => '12',
        'androidInstallApp' => true,
        'iOSBundleId' => 'com.example.ios',
    ];


Email verification
==================

To generate an email verification link, provide the existing user’s unverified email and optional Action Code Settings.
The email used must belong to an existing user. Depending on the method you use, an email will be sent to the user,
or you will get an email action link that you can use in a custom email.

.. code-block:: php

    $link = $auth->getEmailVerificationLink($email);
    $link = $auth->getEmailVerificationLink($email, $actionCodeSettings);
    $link = $auth->getEmailVerificationLink($email, $actionCodeSettings, $locale);

    $auth->sendEmailVerificationLink($email);
    $auth->sendEmailVerificationLink($email, $actionCodeSettings);
    $auth->sendEmailVerificationLink($email, null, $locale);
    $auth->sendEmailVerificationLink($email, $actionCodeSettings, $locale);

Password reset
==============

To generate a password reset link, provide the existing user’s email and optional Action Code Settings.
The email used must belong to an existing user. Depending on the method you use, an email will be sent to the user,
or you will get an email action link that you can use in a custom email.

.. code-block:: php

    $link = $auth->getPasswordResetLink($email);
    $link = $auth->getPasswordResetLink($email, $actionCodeSettings);
    $link = $auth->getPasswordResetLink($email, $actionCodeSettings, $locale);

    $auth->sendPasswordResetLink($email);
    $auth->sendPasswordResetLink($email, $actionCodeSettings);
    $auth->sendPasswordResetLink($email, null, $locale);
    $auth->sendPasswordResetLink($email, $actionCodeSettings, $locale);

Email link for sign-in
======================

.. note::

    Before you can authenticate users with email link sign-in, you will need to enable
    `email link sign-in <https://firebase.google.com/docs/auth/web/email-link-auth#enable_email_link_sign-in_for_your_firebase_project>`_
    for your Firebase project.

.. note::

    Unlike password reset and email verification, the email used does not necessarily need to belong to an existing user,
    as this operation can be used to sign up new users into your app via email link.

.. note::

    The ActionCodeSettings object is required in this case to provide information on where to return the user after the
    link is clicked for sign-in completion.

To generate a sign-in link, provide the user’s email and Action Code Settings. Depending on the method you use,
an email will be sent to the user, or you will get an email action link that you can use in a custom email.

.. code-block:: php

    $link = $auth->getSignInWithEmailLink($email, $actionCodeSettings);

    $auth->sendSignInWithEmailLink($email, $actionCodeSettings);
    $auth->sendSignInWithEmailLink($email, $actionCodeSettings, $locale);

Confirm a password reset
========================

.. note::
    Out of the box, Firebase handles the confirmation of password reset requests. You can use your own
    server to handle account management emails by following the instructions on
    `Customize account management emails and SMS messages <https://support.google.com/firebase/answer/7000714>`_

.. code-block:: php

    $oobCode = '...'; // Extract the OOB code from the request url (not scope of the SDK (yet :)))
    $newPassword = '...';
    $invalidatePreviousSessions = true; // default, will revoke current user refresh tokens

    try {
        $auth->confirmPasswordReset($oobCode, $newPassword, $invalidatePreviousSessions);
    } catch (\Kreait\Firebase\Exception\Auth\ExpiredOobCode $e) {
        // Handle the case of an expired reset code
    } catch (\Kreait\Firebase\Exception\Auth\InvalidOobCode $e) {
        // Handle the case of an invalid reset code
    } catch (\Kreait\Firebase\Exception\AuthException $e) {
        // Another error has occurred
    }

