Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement before_email_sent and before_sms_sent blocking functions #222

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions samples/identity/functions/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def beforeusercreated(
event: identity_fn.AuthBlockingEvent
) -> identity_fn.BeforeCreateResponse | None:
print(event)
if not event.data.email:
if not event.data or not event.data.email:
return None
if "@cats.com" in event.data.email:
return identity_fn.BeforeCreateResponse(display_name="Meow!",)
Expand All @@ -29,7 +29,7 @@ def beforeusersignedin(
event: identity_fn.AuthBlockingEvent
) -> identity_fn.BeforeSignInResponse | None:
print(event)
if not event.data.email:
if not event.data or not event.data.email:
return None

if "@cats.com" in event.data.email:
Expand All @@ -39,3 +39,21 @@ def beforeusersignedin(
return identity_fn.BeforeSignInResponse(session_claims={"emoji": "🐕"})

return None


@identity_fn.before_email_sent()
# pylint: disable=useless-return
def beforeemailsent(
event: identity_fn.AuthBlockingEvent
) -> identity_fn.BeforeEmailSentResponse | None:
print(event)
return None


@identity_fn.before_sms_sent()
# pylint: disable=useless-return
def beforesmssent(
event: identity_fn.AuthBlockingEvent
) -> identity_fn.BeforeSmsSentResponse | None:
print(event)
return None
4 changes: 2 additions & 2 deletions samples/identity/functions/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Not published yet,
# firebase-functions-python >= 0.0.1
# so we use a relative path during development:
./../../../
# ./../../../
# Or switch to git ref for deployment testing:
# git+https://github.com/firebase/firebase-functions-python.git@main#egg=firebase-functions
exaby73 marked this conversation as resolved.
Show resolved Hide resolved
git+https://github.com/firebase/firebase-functions-python.git@feat/before_email_sms_sent_blocking_fn#egg=firebase-functions

firebase-admin >= 6.0.1
7 changes: 5 additions & 2 deletions src/firebase_functions/firestore_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,11 @@ def _firestore_endpoint_handler(
auth_id=event_auth_id)
func(database_event_with_auth_context)
else:
# mypy cannot infer that the event type is correct, hence the cast
_typing.cast(_C1 | _C2, func)(database_event)
# Split the casting into two separate branches based on event type
if event_type in (_event_type_written, _event_type_updated):
_typing.cast(_C1, func)(_typing.cast(_E1, database_event))
else:
_typing.cast(_C2, func)(_typing.cast(_E2, database_event))


@_util.copy_func_kwargs(FirestoreOptions)
Expand Down
160 changes: 158 additions & 2 deletions src/firebase_functions/identity_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ class AdditionalUserInfo:
The additional user info component of the auth event context.
"""

provider_id: str
provider_id: str | None
"""The provider identifier."""

profile: dict[str, _typing.Any] | None
Expand All @@ -206,6 +206,12 @@ class AdditionalUserInfo:
recaptcha_score: float | None
"""The user's reCAPTCHA score, if available."""

email: str | None
"""The user's email, if available."""

phone_number: str | None
"""The user's phone number, if available."""


@_dataclasses.dataclass(frozen=True)
class Credential:
Expand Down Expand Up @@ -238,13 +244,18 @@ class Credential:
"""The user's sign-in method."""


EmailType = _typing.Literal["EMAIL_SIGN_IN", "PASSWORD_RESET"]
SmsType = _typing.Literal["SIGN_IN_OR_SIGN_UP", "MULTI_FACTOR_SIGN_IN",
"MULTI_FACTOR_ENROLLMENT"]


@_dataclasses.dataclass(frozen=True)
class AuthBlockingEvent:
"""
Defines an auth event for identitytoolkit v2 auth blocking events.
"""

data: AuthUserRecord
data: AuthUserRecord | None # This is None for beforeEmailSent and beforeSmsSent events
"""
The UserRecord passed to auth blocking functions from the identity platform.
"""
Expand Down Expand Up @@ -280,6 +291,12 @@ class AuthBlockingEvent:
credential: Credential | None
"""An object containing information about the user's credential."""

email_type: EmailType | None
"""The type of email event."""

sms_type: SmsType | None
"""The type of SMS event."""

timestamp: _dt.datetime
"""
The time the event was triggered."""
Expand Down Expand Up @@ -323,6 +340,22 @@ class BeforeSignInResponse(BeforeCreateResponse, total=False):
"""The user's session claims object if available."""


class BeforeEmailSentResponse(_typing.TypedDict, total=False):
"""
The handler response type for 'before_email_sent' blocking events.
"""

recaptcha_action_override: RecaptchaActionOptions | None


class BeforeSmsSentResponse(_typing.TypedDict, total=False):
"""
The handler response type for 'before_sms_sent' blocking events.
"""

recaptcha_action_override: RecaptchaActionOptions | None


BeforeUserCreatedCallable = _typing.Callable[[AuthBlockingEvent],
BeforeCreateResponse | None]
"""
Expand All @@ -335,6 +368,18 @@ class BeforeSignInResponse(BeforeCreateResponse, total=False):
The type of the callable for 'before_user_signed_in' blocking events.
"""

BeforeEmailSentCallable = _typing.Callable[[AuthBlockingEvent],
BeforeEmailSentResponse | None]
"""
The type of the callable for 'before_email_sent' blocking events.
"""

BeforeSmsSentCallable = _typing.Callable[[AuthBlockingEvent],
BeforeSmsSentResponse | None]
"""
The type of the callable for 'before_sms_sent' blocking events.
"""


@_util.copy_func_kwargs(_options.BlockingOptions)
def before_user_signed_in(
Expand Down Expand Up @@ -442,3 +487,114 @@ def before_user_created_wrapped(request: _Request) -> _Response:
return before_user_created_wrapped

return before_user_created_decorator


@_util.copy_func_kwargs(_options.BaseBlockingOptions)
def before_email_sent(
**kwargs,
) -> _typing.Callable[[BeforeEmailSentCallable], BeforeEmailSentCallable]:
"""
Handles an event that is triggered before a user's email is sent.

Example:

.. code-block:: python

from firebase_functions import identity_fn

@identity_fn.before_email_sent()
def example(
event: identity_fn.AuthBlockingEvent
) -> identity_fn.BeforeEmailSentResponse | None:
pass

:param \\*\\*kwargs: Options.
:type \\*\\*kwargs: as :exc:`firebase_functions.options.BaseBlockingOptions`
:rtype: :exc:`typing.Callable`
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthBlockingEvent` \\],
:exc:`firebase_functions.identity_fn.BeforeEmailSentResponse` \\| `None` \\]
A function that takes a AuthBlockingEvent and optionally returns
BeforeEmailSentResponse.
"""
options = _options.BaseBlockingOptions(**kwargs)

def before_email_sent_decorator(func: BeforeEmailSentCallable):
from firebase_functions.private._identity_fn_event_types import event_type_before_email_sent

@_functools.wraps(func)
def before_email_sent_wrapped(request: _Request) -> _Response:
from firebase_functions.private._identity_fn import before_operation_handler
return before_operation_handler(
func,
event_type_before_email_sent,
request,
)

_util.set_func_endpoint_attr(
before_email_sent_wrapped,
options._endpoint(
func_name=func.__name__,
event_type=event_type_before_email_sent,
),
)
_util.set_required_apis_attr(
before_email_sent_wrapped,
options._required_apis(),
)
return before_email_sent_wrapped

return before_email_sent_decorator


@_util.copy_func_kwargs(_options.BaseBlockingOptions)
def before_sms_sent(
**kwargs,
) -> _typing.Callable[[BeforeSmsSentCallable], BeforeSmsSentCallable]:
"""
Handles an event that is triggered before a user's SMS is sent.

Example:

.. code-block:: python

from firebase_functions import identity_fn

@identity_fn.before_sms_sent()
def example(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSmsSentResponse | None:
pass

:param \\*\\*kwargs: Options.
:type \\*\\*kwargs: as :exc:`firebase_functions.options.BaseBlockingOptions`
:rtype: :exc:`typing.Callable`
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthBlockingEvent` \\],
:exc:`firebase_functions.identity_fn.BeforeSmsSentResponse` \\| `None` \\]
A function that takes a AuthBlockingEvent and optionally returns BeforeSmsSentResponse.
"""
options = _options.BaseBlockingOptions(**kwargs)

def before_sms_sent_decorator(func: BeforeSmsSentCallable):
from firebase_functions.private._identity_fn_event_types import event_type_before_sms_sent

@_functools.wraps(func)
def before_sms_sent_wrapped(request: _Request) -> _Response:
from firebase_functions.private._identity_fn import before_operation_handler
return before_operation_handler(
func,
event_type_before_sms_sent,
request,
)

_util.set_func_endpoint_attr(
before_sms_sent_wrapped,
options._endpoint(
func_name=func.__name__,
event_type=event_type_before_sms_sent,
),
)
_util.set_required_apis_attr(
before_sms_sent_wrapped,
options._required_apis(),
)
return before_sms_sent_wrapped

return before_sms_sent_decorator
68 changes: 44 additions & 24 deletions src/firebase_functions/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,42 +969,39 @@ def _endpoint(


@_dataclasses.dataclass(frozen=True, kw_only=True)
class BlockingOptions(RuntimeOptions):
class BaseBlockingOptions(RuntimeOptions):
"""
Options that can be set on an Auth Blocking trigger.
Base class for options that can be set on an Auth Blocking trigger.
Internal use only.
"""

id_token: bool | None = None
"""
Pass the ID Token credential to the function.
"""

access_token: bool | None = None
"""
Pass the access token credential to the function.
"""

refresh_token: bool | None = None
"""
Pass the refresh token credential to the function.
"""

def _endpoint(
self,
**kwargs,
) -> _manifest.ManifestEndpoint:
from firebase_functions.private._identity_fn_event_types import event_type_before_create, event_type_before_sign_in

assert kwargs["event_type"] is not None

blocking_trigger_options: _manifest.BlockingTriggerOptions

if kwargs["event_type"] == event_type_before_create or kwargs[
"event_type"] == event_type_before_sign_in:
options = _typing.cast(BlockingOptions, self)
blocking_trigger_options = _manifest.BlockingTriggerOptions(
idToken=options.id_token
if options.id_token is not None else False,
accessToken=options.access_token
if options.access_token is not None else False,
refreshToken=options.refresh_token
if options.refresh_token is not None else False,
)
else:
blocking_trigger_options = _manifest.BlockingTriggerOptions()

blocking_trigger = _manifest.BlockingTrigger(
eventType=kwargs["event_type"],
options=_manifest.BlockingTriggerOptions(
idToken=self.id_token if self.id_token is not None else False,
accessToken=self.access_token
if self.access_token is not None else False,
refreshToken=self.refresh_token
if self.refresh_token is not None else False,
),
options=blocking_trigger_options,
)

kwargs_merged = {
Expand All @@ -1024,6 +1021,29 @@ def _required_apis(self) -> list[_manifest.ManifestRequiredApi]:
]


@_dataclasses.dataclass(frozen=True, kw_only=True)
class BlockingOptions(BaseBlockingOptions):
"""
Options that can be set on an Auth Blocking trigger.
Internal use only.
"""

id_token: bool | None = None
"""
Pass the ID Token credential to the function.
"""

access_token: bool | None = None
"""
Pass the access token credential to the function.
"""

refresh_token: bool | None = None
"""
Pass the refresh token credential to the function.
"""


@_dataclasses.dataclass(frozen=True, kw_only=True)
class FirestoreOptions(RuntimeOptions):
"""
Expand Down
Loading
Loading