1414
1515import jwt
1616from bson import ObjectId
17- from flask import Blueprint , Response , g , jsonify , redirect , request
17+ from flask import Blueprint , g , jsonify , redirect , request
1818from flask_dance .consumer import OAuth2ConsumerBlueprint , oauth_authorized
1919from flask_login import current_user , login_user
2020from flask_login .utils import LocalProxy
21- from werkzeug .exceptions import BadRequest
21+ from werkzeug .exceptions import BadRequest , Forbidden
2222
2323from pydatalab .config import CONFIG
2424from pydatalab .errors import UserRegistrationForbidden
@@ -253,6 +253,9 @@ def _check_email_domain(email: str, allow_list: list[str] | None) -> bool:
253253 Whether the email address is allowed to register an account.
254254
255255 """
256+ if CONFIG .TESTING :
257+ return True
258+
256259 domain = email .split ("@" )[- 1 ]
257260 if isinstance (allow_list , list ) and not allow_list :
258261 return False
@@ -420,48 +423,36 @@ def attach_identity_to_user(
420423 wrapped_login_user (get_by_id (str (user .immutable_id )))
421424
422425
423- def _validate_magic_link_request (email : str , referrer : str ) -> tuple [ Response | None , int ] :
426+ def _validate_magic_link_request (email : str , referrer : str ) -> None :
424427 if not email :
425- return jsonify ({ "status" : "error" , "detail" : " No email provided." }), 400
428+ raise BadRequest ( " No email provided" )
426429
427430 if not re .match (r"^\S+@\S+.\S+$" , email ):
428- return jsonify ({ "status" : "error" , "detail" : " Invalid email provided."}), 400
431+ raise BadRequest ( " Invalid email provided.")
429432
430433 if not referrer :
431- LOGGER .warning ("No referrer provided for magic link request" )
432- return (
433- jsonify (
434- {
435- "status" : "error" ,
436- "detail" : "Referrer address not provided, please contact the datalab administrator." ,
437- }
438- ),
439- 400 ,
440- )
441-
442- return None , 200
434+ raise BadRequest ("Referrer address not provided, please contact the datalab administrator" )
443435
444436
445- def _generate_and_store_token (email : str , is_test : bool = False ) -> str :
437+ def _generate_and_store_token (email : str , intent : str = "register" ) -> str :
446438 """Generate a JWT for the user with a short expiration and store it in the session.
447439
448440 The session itself persists beyond the JWT expiration. The `exp` key is a standard
449441 part of JWT that PyJWT treats as an expiration time and will correctly encode the datetime.
450442
451443 Args:
452444 email: The user's email address to include in the token.
453- is_test: If True, generates a token for testing purposes that may have different
454- expiration or validation rules. Defaults to False.
445+ intent: The intent of the magic link, e.g., "register" "verify", or "login".
455446
456447 Returns:
457448 The generated JWT token string.
449+
458450 """
459451 payload = {
460452 "exp" : datetime .datetime .now (datetime .timezone .utc ) + LINK_EXPIRATION ,
461453 "email" : email ,
454+ "intent" : intent ,
462455 }
463- if is_test :
464- payload ["is_test" ] = True
465456
466457 token = jwt .encode (
467458 payload ,
@@ -474,45 +465,49 @@ def _generate_and_store_token(email: str, is_test: bool = False) -> str:
474465 return token
475466
476467
477- def _check_user_registration_allowed (email : str ) -> tuple [ Response | None , int ] :
468+ def _check_user_registration_allowed (email : str ) -> None :
478469 user = find_user_with_identity (email , IdentityType .EMAIL , verify = False )
479470
480471 if not user :
481472 allowed = _check_email_domain (email , CONFIG .EMAIL_DOMAIN_ALLOW_LIST )
482473 if not allowed :
483- LOGGER .info ("Did not allow %s to register an account" , email )
484- return (
485- jsonify (
486- {
487- "status" : "error" ,
488- "detail" : f"Email address { email } is not allowed to register an account. Please contact the administrator if you believe this is an error." ,
489- }
490- ),
491- 403 ,
474+ LOGGER .warning ("Did not allow %s to register an account" , email )
475+ raise Forbidden (
476+ f"Email address { email } is not allowed to register an account. Please contact the administrator if you believe this is an error."
492477 )
493478
494- return None , 200
495479
480+ def _send_magic_link_email (
481+ email : str , token : str , referrer : str | None , purpose : str = "authorize"
482+ ) -> None :
483+ if not referrer :
484+ referrer = "https://example.com"
496485
497- def _send_magic_link_email (email : str , token : str , referrer : str ) -> tuple [Response | None , int ]:
498486 link = f"{ referrer } ?token={ token } "
499487 instance_url = referrer .replace ("https://" , "" )
500- user = find_user_with_identity (email , IdentityType .EMAIL , verify = False )
501488
502- if user is not None :
503- subject = "Datalab Sign-in Magic Link"
504- body = f"Click the link below to sign-in to the datalab instance at { instance_url } :\n \n { link } \n \n This link is single-use and will expire in 1 hour."
489+ if purpose == "authorize" :
490+ user = find_user_with_identity (email , IdentityType .EMAIL , verify = False )
491+ if user is not None :
492+ subject = "datalab Sign-in Magic Link"
493+ body = f"Click the link below to sign-in to the datalab instance at { instance_url } :\n \n { link } \n \n This link is single-use and will expire in 1 hour."
494+ else :
495+ subject = "datalab Registration Magic Link"
496+ body = f"Click the link below to register for the datalab instance at { instance_url } :\n \n { link } \n \n This link is single-use and will expire in 1 hour."
497+
498+ elif purpose == "verify" :
499+ subject = "datalab Email Address Verification"
500+ body = f"Click the link below to verify your email address for the datalab instance at { instance_url } :\n \n { link } \n \n This link is single-use and will expire in 1 hour."
501+
505502 else :
506- subject = "Datalab Registration Magic Link"
507- body = f"Click the link below to register for the datalab instance at { instance_url } : \n \n { link } \n \n This link is single-use and will expire in 1 hour."
503+ LOGGER . critical ( "Unknown purpose %s for magic link email" , purpose )
504+ raise RuntimeError ( "Unknown error occurred" )
508505
509506 try :
510507 send_mail (email , subject , body )
511508 except Exception as exc :
512509 LOGGER .warning ("Failed to send email to %s: %s" , email , exc )
513- return jsonify ({"status" : "error" , "detail" : "Email not sent successfully." }), 400
514-
515- return None , 200
510+ raise RuntimeError ("Email not sent successfully." )
516511
517512
518513@EMAIL_BLUEPRINT .route ("/magic-link" , methods = ["POST" ])
@@ -526,21 +521,12 @@ def generate_and_share_magic_link():
526521 email = request_json .get ("email" )
527522 referrer = request_json .get ("referrer" )
528523
529- error_response , status_code = _validate_magic_link_request (email , referrer )
530- if error_response :
531- return error_response , status_code
532-
533- error_response , status_code = _check_user_registration_allowed (email )
534- if error_response :
535- return error_response , status_code
524+ _validate_magic_link_request (email , referrer )
525+ _check_user_registration_allowed (email )
526+ token = _generate_and_store_token (email , intent = "register" )
527+ _send_magic_link_email (email , token , referrer )
536528
537- token = _generate_and_store_token (email )
538-
539- error_response , status_code = _send_magic_link_email (email , token , referrer )
540- if error_response :
541- return error_response , status_code
542-
543- return jsonify ({"status" : "success" , "detail" : "Email sent successfully." }), 200
529+ return jsonify ({"status" : "success" , "message" : "Email sent successfully." }), 200
544530
545531
546532@EMAIL_BLUEPRINT .route ("/email" )
@@ -580,21 +566,22 @@ def email_logged_in():
580566 # If the email domain list is explicitly configured to None, this allows any
581567 # email address to make an active account, otherwise the email domain must match
582568 # the list of allowed domains and the admin must verify the user
583- is_test = data .get ("is_test" , False )
584569
585- if not is_test :
570+ if data . get ( "intent" ) == "register" :
586571 allowed = _check_email_domain (email , CONFIG .EMAIL_DOMAIN_ALLOW_LIST )
587572 if not allowed :
588573 raise UserRegistrationForbidden
589574
590- create_account = AccountStatus .UNVERIFIED
591- if (
592- CONFIG .EMAIL_DOMAIN_ALLOW_LIST is None
593- or CONFIG .EMAIL_AUTO_ACTIVATE_ACCOUNTS
594- or CONFIG .AUTO_ACTIVATE_ACCOUNTS
595- or is_test
596- ):
597- create_account = AccountStatus .ACTIVE
575+ create_account = AccountStatus .UNVERIFIED
576+ if (
577+ CONFIG .EMAIL_DOMAIN_ALLOW_LIST is None
578+ or CONFIG .EMAIL_AUTO_ACTIVATE_ACCOUNTS
579+ or CONFIG .AUTO_ACTIVATE_ACCOUNTS
580+ ):
581+ create_account = AccountStatus .ACTIVE
582+
583+ else :
584+ create_account = False
598585
599586 find_create_or_modify_user (
600587 email ,
@@ -751,10 +738,8 @@ def create_test_magic_link():
751738 email = request_json .get ("email" )
752739 referrer = request_json .get ("referrer" , "http://localhost:8080" )
753740
754- error_response , status_code = _validate_magic_link_request (email , referrer )
755- if error_response :
756- return error_response , status_code
741+ _validate_magic_link_request (email , referrer )
757742
758- token = _generate_and_store_token (email , is_test = True )
743+ token = _generate_and_store_token (email , intent = "register" )
759744
760745 return jsonify ({"status" : "success" , "token" : token }), 200
0 commit comments