|
18 | 18 |
|
19 | 19 | import requests
|
20 | 20 | import six
|
| 21 | +from six.moves import urllib |
21 | 22 |
|
22 | 23 | from firebase_admin import _auth_utils
|
23 | 24 | from firebase_admin import _user_import
|
|
30 | 31 | USER_DELETE_ERROR = 'USER_DELETE_ERROR'
|
31 | 32 | USER_IMPORT_ERROR = 'USER_IMPORT_ERROR'
|
32 | 33 | USER_DOWNLOAD_ERROR = 'LIST_USERS_ERROR'
|
| 34 | +GENERATE_EMAIL_ACTION_LINK_ERROR = 'GENERATE_EMAIL_ACTION_LINK_ERROR' |
33 | 35 |
|
34 | 36 | MAX_LIST_USERS_RESULTS = 1000
|
35 | 37 | MAX_IMPORT_USERS_SIZE = 1000
|
@@ -372,6 +374,87 @@ def photo_url(self):
|
372 | 374 | def provider_id(self):
|
373 | 375 | return self._data.get('providerId')
|
374 | 376 |
|
| 377 | +class ActionCodeSettings(object): |
| 378 | + """Contains required continue/state URL with optional Android and iOS settings. |
| 379 | + Used when invoking the email action link generation APIs. |
| 380 | + """ |
| 381 | + |
| 382 | + def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_bundle_id=None, |
| 383 | + android_package_name=None, android_install_app=None, android_minimum_version=None): |
| 384 | + self.url = url |
| 385 | + self.handle_code_in_app = handle_code_in_app |
| 386 | + self.dynamic_link_domain = dynamic_link_domain |
| 387 | + self.ios_bundle_id = ios_bundle_id |
| 388 | + self.android_package_name = android_package_name |
| 389 | + self.android_install_app = android_install_app |
| 390 | + self.android_minimum_version = android_minimum_version |
| 391 | + |
| 392 | +def encode_action_code_settings(settings): |
| 393 | + """ Validates the provided action code settings for email link generation and |
| 394 | + populates the REST api parameters. |
| 395 | +
|
| 396 | + settings - ``ActionCodeSettings`` object provided to be encoded |
| 397 | + returns - dict of parameters to be passed for link gereration. |
| 398 | + """ |
| 399 | + |
| 400 | + parameters = {} |
| 401 | + # url |
| 402 | + if not settings.url: |
| 403 | + raise ValueError("Dynamic action links url is mandatory") |
| 404 | + |
| 405 | + try: |
| 406 | + parsed = urllib.parse.urlparse(settings.url) |
| 407 | + if not parsed.netloc: |
| 408 | + raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) |
| 409 | + parameters['continueUrl'] = settings.url |
| 410 | + except Exception: |
| 411 | + raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) |
| 412 | + |
| 413 | + # handle_code_in_app |
| 414 | + if settings.handle_code_in_app is not None: |
| 415 | + if not isinstance(settings.handle_code_in_app, bool): |
| 416 | + raise ValueError('Invalid value provided for handle_code_in_app: {0}' |
| 417 | + .format(settings.handle_code_in_app)) |
| 418 | + parameters['canHandleCodeInApp'] = settings.handle_code_in_app |
| 419 | + |
| 420 | + # dynamic_link_domain |
| 421 | + if settings.dynamic_link_domain is not None: |
| 422 | + if not isinstance(settings.dynamic_link_domain, six.string_types): |
| 423 | + raise ValueError('Invalid value provided for dynamic_link_domain: {0}' |
| 424 | + .format(settings.dynamic_link_domain)) |
| 425 | + parameters['dynamicLinkDomain'] = settings.dynamic_link_domain |
| 426 | + |
| 427 | + # ios_bundle_id |
| 428 | + if settings.ios_bundle_id is not None: |
| 429 | + if not isinstance(settings.ios_bundle_id, six.string_types): |
| 430 | + raise ValueError('Invalid value provided for ios_bundle_id: {0}' |
| 431 | + .format(settings.ios_bundle_id)) |
| 432 | + parameters['iosBundleId'] = settings.ios_bundle_id |
| 433 | + |
| 434 | + # android_* attributes |
| 435 | + if (settings.android_minimum_version or settings.android_install_app) \ |
| 436 | + and not settings.android_package_name: |
| 437 | + raise ValueError("Android package name is required when specifying other Android settings") |
| 438 | + |
| 439 | + if settings.android_package_name is not None: |
| 440 | + if not isinstance(settings.android_package_name, six.string_types): |
| 441 | + raise ValueError('Invalid value provided for android_package_name: {0}' |
| 442 | + .format(settings.android_package_name)) |
| 443 | + parameters['androidPackageName'] = settings.android_package_name |
| 444 | + |
| 445 | + if settings.android_minimum_version is not None: |
| 446 | + if not isinstance(settings.android_minimum_version, six.string_types): |
| 447 | + raise ValueError('Invalid value provided for android_minimum_version: {0}' |
| 448 | + .format(settings.android_minimum_version)) |
| 449 | + parameters['androidMinimumVersion'] = settings.android_minimum_version |
| 450 | + |
| 451 | + if settings.android_install_app is not None: |
| 452 | + if not isinstance(settings.android_install_app, bool): |
| 453 | + raise ValueError('Invalid value provided for android_install_app: {0}' |
| 454 | + .format(settings.android_install_app)) |
| 455 | + parameters['androidInstallApp'] = settings.android_install_app |
| 456 | + |
| 457 | + return parameters |
375 | 458 |
|
376 | 459 | class UserManager(object):
|
377 | 460 | """Provides methods for interacting with the Google Identity Toolkit."""
|
@@ -537,6 +620,41 @@ def import_users(self, users, hash_alg=None):
|
537 | 620 | raise ApiCallError(USER_IMPORT_ERROR, 'Failed to import users.')
|
538 | 621 | return response
|
539 | 622 |
|
| 623 | + def generate_email_action_link(self, action_type, email, action_code_settings=None): |
| 624 | + """Fetches the email action links for types |
| 625 | +
|
| 626 | + Args: |
| 627 | + action_type: String. Valid values ['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'] |
| 628 | + email: Email of the user for which the action is performed |
| 629 | + action_code_settings: ``ActionCodeSettings`` object or dict (optional). Defines whether |
| 630 | + the link is to be handled by a mobile app and the additional state information to be |
| 631 | + passed in the deep link, etc. |
| 632 | + Returns: |
| 633 | + link_url: action url to be emailed to the user |
| 634 | +
|
| 635 | + Raises: |
| 636 | + ApiCallError: If an error occurs while generating the link |
| 637 | + ValueError: If the provided arguments are invalid |
| 638 | + """ |
| 639 | + payload = { |
| 640 | + 'requestType': _auth_utils.validate_action_type(action_type), |
| 641 | + 'email': _auth_utils.validate_email(email), |
| 642 | + 'returnOobLink': True |
| 643 | + } |
| 644 | + |
| 645 | + if action_code_settings: |
| 646 | + payload.update(encode_action_code_settings(action_code_settings)) |
| 647 | + |
| 648 | + try: |
| 649 | + response = self._client.body('post', '/accounts:sendOobCode', json=payload) |
| 650 | + except requests.exceptions.RequestException as error: |
| 651 | + self._handle_http_error(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.', |
| 652 | + error) |
| 653 | + else: |
| 654 | + if not response or not response.get('oobLink'): |
| 655 | + raise ApiCallError(GENERATE_EMAIL_ACTION_LINK_ERROR, 'Failed to generate link.') |
| 656 | + return response.get('oobLink') |
| 657 | + |
540 | 658 | def _handle_http_error(self, code, msg, error):
|
541 | 659 | if error.response is not None:
|
542 | 660 | msg += '\nServer response: {0}'.format(error.response.content.decode())
|
|
0 commit comments